Replace go-openai with native client
Drop the github.com/sashabaranov/go-openai dependency in favor of an internal HTTP client. The LLM package is now split into: - llm.go: type definitions (Message, ChatRequest, ChatResponse, etc.) - client.go: client construction and configuration - client_completions.go: Completions and CompletionsStream methods - client_models.go: ListModels - client_tools.go: tool call execution CompletionsStream uses typed SSE events (delta, done, message) so the frontend can render progressive streaming output. Move tool.go and tool_test.go from internal/tool/ into internal/llm/ to co-locate the registry with the client that consumes it. Update job.go and service.go to use the new llm.Message types and Completions/CompletionsStream methods. Remove the Voice field from chat request/response (voice is now tracked via message metadata). Frontend: handle delta/done/message SSE events, render a streaming placeholder that receives progressive text deltas, and finalize with full message binding (reasoning, tool calls, actions).
This commit is contained in:
@@ -24,11 +24,9 @@ import (
|
||||
"code.chimeric.al/chimerical/odidere/internal/job"
|
||||
"code.chimeric.al/chimerical/odidere/internal/llm"
|
||||
"code.chimeric.al/chimerical/odidere/internal/service/templates"
|
||||
"code.chimeric.al/chimerical/odidere/internal/tool"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/robfig/cron/v3"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// Context keys for request-scoped values.
|
||||
@@ -57,7 +55,7 @@ type Service struct {
|
||||
mux *http.ServeMux
|
||||
server *http.Server
|
||||
tmpl *template.Template
|
||||
tools *tool.Registry
|
||||
tools *llm.Registry
|
||||
}
|
||||
|
||||
// New creates a Service from the provided configuration.
|
||||
@@ -70,7 +68,7 @@ func New(cfg *config.Config, log *slog.Logger) (*Service, error) {
|
||||
}
|
||||
|
||||
// Setup tool registry.
|
||||
registry, err := tool.NewRegistry(cfg.Tools)
|
||||
registry, err := llm.NewRegistry(cfg.Tools)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load tools: %v", err)
|
||||
}
|
||||
@@ -441,28 +439,24 @@ func (svc *Service) status(w http.ResponseWriter, r *http.Request) {
|
||||
// Request is the incoming request format for the chat endpoints.
|
||||
type Request struct {
|
||||
// Messages is the conversation history.
|
||||
Messages []openai.ChatCompletionMessage `json:"messages"`
|
||||
Messages []llm.Message `json:"messages"`
|
||||
// Provider is the LLM provider name. If empty, the default provider is used.
|
||||
Provider string `json:"provider,omitempty"`
|
||||
// Model is the LLM model ID. If empty, the default model is used.
|
||||
Model string `json:"model,omitempty"`
|
||||
// SystemMessage overrides the configured system message for this request.
|
||||
SystemMessage string `json:"system_message,omitempty"`
|
||||
// Voice is the voice ID for TTS.
|
||||
Voice string `json:"voice,omitempty"`
|
||||
}
|
||||
|
||||
// Response is the response format for chat and voice endpoints.
|
||||
type Response struct {
|
||||
// Messages is the full list of messages generated during the query,
|
||||
// Messages is the full list of messages generated during the completions request,
|
||||
// including tool calls and tool results.
|
||||
Messages []openai.ChatCompletionMessage `json:"messages,omitempty"`
|
||||
Messages []llm.Message `json:"messages,omitempty"`
|
||||
// Provider is the LLM provider used for the response.
|
||||
Provider string `json:"used_provider,omitempty"`
|
||||
// Model is the LLM model used for the response.
|
||||
Model string `json:"used_model,omitempty"`
|
||||
// Voice is the voice used for TTS synthesis.
|
||||
Voice string `json:"used_voice,omitempty"`
|
||||
}
|
||||
|
||||
// chat processes text chat requests.
|
||||
@@ -490,7 +484,9 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "messages required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
log.InfoContext(ctx, "messages",
|
||||
log.DebugContext(
|
||||
ctx,
|
||||
"messages",
|
||||
slog.Any("data", req.Messages),
|
||||
)
|
||||
|
||||
@@ -540,7 +536,9 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
msgs, err := llmc.Query(ctx, req.Messages, model, req.SystemMessage, 0)
|
||||
msgs, err := llmc.Completions(
|
||||
ctx, model, req.SystemMessage, 0, req.Messages,
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(
|
||||
ctx,
|
||||
@@ -559,10 +557,10 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
final := msgs[len(msgs)-1]
|
||||
log.InfoContext(
|
||||
log.DebugContext(
|
||||
ctx,
|
||||
"LLM response",
|
||||
slog.String("text", final.Content),
|
||||
slog.String("text", final.Text()),
|
||||
slog.String("provider", provider),
|
||||
slog.String("model", model),
|
||||
)
|
||||
@@ -572,7 +570,6 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
|
||||
Messages: msgs,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
Voice: req.Voice,
|
||||
}); err != nil {
|
||||
log.ErrorContext(
|
||||
ctx,
|
||||
@@ -586,14 +583,14 @@ func (svc *Service) chat(w http.ResponseWriter, r *http.Request) {
|
||||
type StreamMessage struct {
|
||||
// Error is an error message, if any.
|
||||
Error string `json:"error,omitempty"`
|
||||
// Delta is a text fragment from the assistant response.
|
||||
Delta string `json:"delta,omitempty"`
|
||||
// Message is the chat completion message.
|
||||
Message openai.ChatCompletionMessage `json:"message"`
|
||||
Message *llm.Message `json:"message,omitempty"`
|
||||
// Provider is the LLM provider used for the response.
|
||||
Provider string `json:"provider,omitempty"`
|
||||
// Model is the LLM model used for the response.
|
||||
Model string `json:"model,omitempty"`
|
||||
// Voice is the voice used for TTS synthesis.
|
||||
Voice string `json:"voice,omitempty"`
|
||||
}
|
||||
|
||||
// chatStream processes chat requests with streaming SSE output.
|
||||
@@ -685,7 +682,7 @@ func (svc *Service) chatStream(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
// Helper to send an SSE event.
|
||||
send := func(msg StreamMessage) {
|
||||
send := func(event string, msg StreamMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, "failed to marshal SSE event",
|
||||
@@ -693,54 +690,47 @@ func (svc *Service) chatStream(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", data)
|
||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// Start streaming LLM query.
|
||||
// Start streaming LLM completions request.
|
||||
var (
|
||||
events = make(chan llm.StreamEvent)
|
||||
llmErr error
|
||||
errs = make(chan error, 1)
|
||||
)
|
||||
go func() {
|
||||
llmErr = llmc.QueryStream(
|
||||
ctx, req.Messages, model, req.SystemMessage, 0, events,
|
||||
errs <- llmc.CompletionsStream(
|
||||
ctx, model, req.SystemMessage, 0, req.Messages, events,
|
||||
)
|
||||
close(errs)
|
||||
}()
|
||||
|
||||
// Consume events and send as SSE.
|
||||
var last StreamMessage
|
||||
for evt := range events {
|
||||
msg := StreamMessage{Message: evt.Message}
|
||||
|
||||
// Track the last assistant message for TTS.
|
||||
if evt.Message.Role == openai.ChatMessageRoleAssistant &&
|
||||
len(evt.Message.ToolCalls) == 0 {
|
||||
last = msg
|
||||
continue
|
||||
msg := StreamMessage{
|
||||
Delta: evt.Delta,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
}
|
||||
|
||||
send(msg)
|
||||
if evt.Message.Role != "" {
|
||||
msg.Message = &evt.Message
|
||||
}
|
||||
send(evt.Type, msg)
|
||||
}
|
||||
// Check for LLM errors.
|
||||
if llmErr != nil {
|
||||
if err := <-errs; err != nil {
|
||||
log.ErrorContext(
|
||||
ctx,
|
||||
"LLM stream failed",
|
||||
slog.Any("error", llmErr),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
send(StreamMessage{
|
||||
Message: openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
},
|
||||
Error: fmt.Sprintf("LLM error: %v", llmErr),
|
||||
msg := llm.Message{Role: llm.RoleAssistant}
|
||||
send("message", StreamMessage{
|
||||
Message: &msg,
|
||||
Error: fmt.Sprintf("LLM error: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
last.Provider = provider
|
||||
last.Model = model
|
||||
last.Voice = req.Voice
|
||||
send(last)
|
||||
}
|
||||
|
||||
// ModelStatus represents the availability status of a model.
|
||||
|
||||
Reference in New Issue
Block a user