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:
dwrz
2026-06-26 14:22:05 +00:00
parent e480538ec2
commit f6fb21d40c
56 changed files with 1671 additions and 7854 deletions

82
internal/llm/client.go Normal file
View File

@@ -0,0 +1,82 @@
package llm
import (
"fmt"
"log/slog"
"net/http"
"time"
)
// Config holds the configuration for an LLM client.
type Config struct {
// Key is the API key for authentication.
Key string `yaml:"key"`
// SystemMessage is prepended to all conversations.
SystemMessage string `yaml:"system_message"`
// Timeout is the maximum duration for a query (e.g., "5m").
// Defaults to 5 minutes if empty.
Timeout string `yaml:"timeout"`
// URL is the base URL of the OpenAI-compatible API endpoint.
URL string `yaml:"url"`
}
// Validate checks that required configuration values are present and valid.
func (cfg Config) Validate() error {
if cfg.Timeout != "" {
if _, err := time.ParseDuration(cfg.Timeout); err != nil {
return fmt.Errorf("invalid timeout: %w", err)
}
}
if cfg.URL == "" {
return fmt.Errorf("missing URL")
}
return nil
}
// Client wraps an OpenAI-compatible client with tool execution support.
type Client struct {
baseURL string
httpc *http.Client
key string
log *slog.Logger
registry *Registry
systemMessage string
timeout time.Duration
tools []APITool
}
// NewClient creates a new LLM client with the provided configuration.
// The registry is optional; if nil, tool calling is disabled.
func NewClient(
cfg Config,
registry *Registry,
log *slog.Logger,
) (*Client, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
timeout := 5 * time.Minute
if cfg.Timeout != "" {
d, err := time.ParseDuration(cfg.Timeout)
if err != nil {
return nil, fmt.Errorf("parse timeout: %v", err)
}
timeout = d
}
llm := &Client{
baseURL: cfg.URL,
key: cfg.Key,
log: log,
systemMessage: cfg.SystemMessage,
registry: registry,
httpc: &http.Client{},
timeout: timeout,
}
// Convert tools from registry.
llm.tools = registry.ToAPI()
return llm, nil
}