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:
92
internal/llm/client_models.go
Normal file
92
internal/llm/client_models.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ListModels returns available models from the LLM server.
|
||||
func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx, http.MethodGet, c.baseURL+"/models", nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
if c.key != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.key)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := c.httpc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list models: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
c.log.ErrorContext(
|
||||
ctx,
|
||||
"list models: failed to read error response body",
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return nil, fmt.Errorf(
|
||||
"list models: status code %d", res.StatusCode,
|
||||
)
|
||||
}
|
||||
var apiError struct {
|
||||
Error *struct {
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(
|
||||
body, &apiError,
|
||||
); err == nil && apiError.Error != nil {
|
||||
c.log.ErrorContext(
|
||||
ctx,
|
||||
"list models: request error",
|
||||
slog.Int("status", res.StatusCode),
|
||||
slog.String("code", apiError.Error.Code),
|
||||
slog.String(
|
||||
"message", apiError.Error.Message,
|
||||
),
|
||||
slog.String("type", apiError.Error.Type),
|
||||
)
|
||||
return nil, fmt.Errorf(
|
||||
"list models: %s: %s",
|
||||
apiError.Error.Code,
|
||||
apiError.Error.Message,
|
||||
)
|
||||
}
|
||||
c.log.ErrorContext(
|
||||
ctx,
|
||||
"list models: API error",
|
||||
slog.Int("status", res.StatusCode),
|
||||
slog.String("body", string(body)),
|
||||
)
|
||||
return nil, fmt.Errorf(
|
||||
"list models: %d: %s",
|
||||
res.StatusCode, string(body),
|
||||
)
|
||||
}
|
||||
|
||||
var body ModelsResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"list models: decode response: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return body.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user