1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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,
},
}
}
|