summaryrefslogtreecommitdiff
path: root/internal/provider/openaicompat
diff options
context:
space:
mode:
Diffstat (limited to 'internal/provider/openaicompat')
-rw-r--r--internal/provider/openaicompat/openaicompat.go140
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,
+ },
+ }
+}