diff options
Diffstat (limited to 'internal/provider/openaicompat')
| -rw-r--r-- | internal/provider/openaicompat/openaicompat.go | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/provider/openaicompat/openaicompat.go b/internal/provider/openaicompat/openaicompat.go new file mode 100644 index 0000000..164ebeb --- /dev/null +++ b/internal/provider/openaicompat/openaicompat.go @@ -0,0 +1,140 @@ +// Package openaicompat adapts the existing internal/llm.Client (a small +// OpenAI-compatible chat-completions HTTP client) to the provider-neutral +// provider.Provider interface, unchanged in its own behavior — this package +// only translates request/response shapes. +package openaicompat + +import ( + "context" + "fmt" + + "github.com/thepeterstone/claudomator/internal/llm" + "github.com/thepeterstone/claudomator/internal/provider" +) + +// Provider wraps an *llm.Client so it can be driven through the +// provider-neutral interface. +type Provider struct { + Client *llm.Client +} + +// New returns a provider.Provider backed by client. +func New(client *llm.Client) *Provider { + return &Provider{Client: client} +} + +var _ provider.Provider = (*Provider)(nil) + +func (p *Provider) Name() string { return "openaicompat" } + +// Chat translates req into an llm.ChatRequest, performs the call via the +// wrapped client, and translates the result back. +func (p *Provider) Chat(ctx context.Context, req provider.ChatRequest) (*provider.ChatResponse, error) { + if p == nil || p.Client == nil { + return nil, fmt.Errorf("openaicompat: nil client") + } + llmReq := toLLMRequest(req) + resp, err := p.Client.Chat(ctx, llmReq) + if err != nil { + return nil, err + } + return fromLLMResponse(resp), nil +} + +// toLLMRequest translates a provider-neutral ChatRequest into the wire shape +// llm.Client understands. System, if set, becomes a leading role:"system" +// message — llm.Client/the OpenAI-compatible wire format has no separate +// top-level system field. +func toLLMRequest(req provider.ChatRequest) llm.ChatRequest { + messages := make([]llm.Message, 0, len(req.Messages)+1) + if req.System != "" { + messages = append(messages, llm.Message{Role: "system", Content: req.System}) + } + for _, m := range req.Messages { + messages = append(messages, toLLMMessages(m)...) + } + + var tools []llm.Tool + if len(req.Tools) > 0 { + tools = make([]llm.Tool, 0, len(req.Tools)) + for _, ts := range req.Tools { + tools = append(tools, llm.Tool{ + Type: "function", + Function: llm.ToolFunction{ + Name: ts.Name, + Description: ts.Description, + Parameters: ts.ParametersJSONSchema, + }, + }) + } + } + + return llm.ChatRequest{ + Model: req.Model, + Messages: messages, + Temperature: req.Temperature, + MaxTokens: req.MaxTokens, + Tools: tools, + } +} + +// toLLMMessages translates a single provider-neutral Message into zero or more +// llm.Message values. Assistant turns (with ToolCalls) and plain text turns +// translate 1:1. Tool-result turns translate to one llm.Message per +// ToolResult, since the OpenAI wire format represents each tool result as its +// own role:"tool" message (agentloop always emits one ToolResult per turn +// today, matching that shape exactly; the loop here is future-proofing for +// providers/loops that batch multiple results into one turn). +func toLLMMessages(m provider.Message) []llm.Message { + if len(m.ToolResults) > 0 { + out := make([]llm.Message, 0, len(m.ToolResults)) + for _, tr := range m.ToolResults { + out = append(out, llm.Message{ + Role: "tool", + ToolCallID: tr.ToolCallID, + Name: tr.Name, + Content: tr.Content, + }) + } + return out + } + + lm := llm.Message{Role: m.Role, Content: m.Text} + if len(m.ToolCalls) > 0 { + lm.ToolCalls = make([]llm.ToolCall, 0, len(m.ToolCalls)) + for _, tc := range m.ToolCalls { + lm.ToolCalls = append(lm.ToolCalls, llm.ToolCall{ + ID: tc.ID, + Type: "function", + Function: llm.ToolCallFunction{ + Name: tc.Name, + Arguments: tc.ArgsJSON, + }, + }) + } + } + return []llm.Message{lm} +} + +func fromLLMResponse(r *llm.ChatResponse) *provider.ChatResponse { + var calls []provider.ToolCall + if len(r.ToolCalls) > 0 { + calls = make([]provider.ToolCall, 0, len(r.ToolCalls)) + for _, tc := range r.ToolCalls { + calls = append(calls, provider.ToolCall{ + ID: tc.ID, + Name: tc.Function.Name, + ArgsJSON: tc.Function.Arguments, + }) + } + } + return &provider.ChatResponse{ + Text: r.Content, + ToolCalls: calls, + StopReason: r.FinishReason, + Usage: provider.Usage{ + InputTokens: r.PromptTokens, + OutputTokens: r.OutputTokens, + }, + } +} |
