1// Package provider defines the LLM client abstraction used by zot.
2//
3// It supports exactly two providers: Anthropic (Messages API) and
4// OpenAI (Chat Completions API). Everything above this package operates
5// on the types declared here and does not know about HTTP or SSE.
6package provider
7
8import (
9 "context"
10 "encoding/json"
11 "time"
12)
13
14// Role is the speaker of a Message.
15type Role string
16
17const (
18 RoleUser Role = "user"
19 RoleAssistant Role = "assistant"
20 RoleTool Role = "tool"
21)
22
23// Content is a block inside a Message. One of TextBlock, ImageBlock,
24// ToolCallBlock, or ToolResultBlock.
25type Content interface {
26 isContent()
27}
28
29// TextBlock is plain text content.
30type TextBlock struct {
31 Text string `json:"text"`
32}
33
34func (TextBlock) isContent() {}
35
36// ImageBlock is an inline image (PNG/JPEG/GIF/WebP).
37type ImageBlock struct {
38 MimeType string `json:"mime_type"`
39 Data []byte `json:"data"` // raw bytes; encoded to base64 on the wire
40}
41
42func (ImageBlock) isContent() {}
43
44// ToolCallBlock is an assistant-issued call to a tool.
45type ToolCallBlock struct {
46 ID string `json:"id"`
47 Name string `json:"name"`
48 Arguments json.RawMessage `json:"arguments"`
49}
50
51func (ToolCallBlock) isContent() {}
52
53// ToolResultBlock is the result of a tool execution, attached to a
54// Message with Role == RoleTool.
55type ToolResultBlock struct {
56 CallID string `json:"call_id"`
57 Content []Content `json:"content"`
58 IsError bool `json:"is_error"`
59}
60
61func (ToolResultBlock) isContent() {}
62
63// ReasoningBlock carries the assistant's chain-of-thought metadata so
64// providers that require it on follow-up requests (OpenAI Codex with
65// thinking enabled) can replay the same payload they emitted earlier.
66// Summary is the human-readable reasoning summary (may be empty); the
67// encrypted blob is opaque to zot. ID is the provider-issued reasoning
68// item id.
69type ReasoningBlock struct {
70 ID string `json:"reasoning_id,omitempty"`
71 Summary string `json:"summary,omitempty"`
72 Encrypted string `json:"encrypted_content,omitempty"`
73}
74
75func (ReasoningBlock) isContent() {}
76
77// RepairOrphanedToolResults removes tool_result content blocks (and
78// entire messages that become empty) when the matching tool_use ID
79// does not appear anywhere in the given messages. Resume tails,
80// compaction repair, and provider request builders all need this so
81// the upstream API never sees a tool_call_id with no corresponding
82// assistant tool_call earlier in the same request.
83func RepairOrphanedToolResults(msgs []Message) []Message {
84 useIDs := map[string]bool{}
85 for _, m := range msgs {
86 for _, c := range m.Content {
87 if tc, ok := c.(ToolCallBlock); ok {
88 useIDs[tc.ID] = true
89 }
90 }
91 }
92 out := make([]Message, 0, len(msgs))
93 for _, m := range msgs {
94 var filtered []Content
95 for _, c := range m.Content {
96 if tr, ok := c.(ToolResultBlock); ok {
97 if !useIDs[tr.CallID] {
98 continue
99 }
100 }
101 filtered = append(filtered, c)
102 }
103 if len(filtered) > 0 {
104 copy := m
105 copy.Content = filtered
106 out = append(out, copy)
107 }
108 }
109 return out
110}
111
112// Message is a single turn in the conversation.
113type Message struct {
114 Role Role `json:"role"`
115 Content []Content `json:"content"`
116 Time time.Time `json:"time"`
117 Meta map[string]string `json:"meta,omitempty"`
118}
119
120// Tool is a tool definition advertised to the LLM.
121type Tool struct {
122 Name string `json:"name"`
123 Description string `json:"description"`
124 Schema json.RawMessage `json:"schema"` // JSON Schema for arguments
125}
126
127// Usage aggregates token counts and cost for a turn.
128type Usage struct {
129 InputTokens int `json:"input_tokens"`
130 OutputTokens int `json:"output_tokens"`
131 CacheReadTokens int `json:"cache_read_tokens"`
132 CacheWriteTokens int `json:"cache_write_tokens"`
133 CostUSD float64 `json:"cost_usd"`
134}
135
136// Add returns u plus v.
137func (u Usage) Add(v Usage) Usage {
138 return Usage{
139 InputTokens: u.InputTokens + v.InputTokens,
140 OutputTokens: u.OutputTokens + v.OutputTokens,
141 CacheReadTokens: u.CacheReadTokens + v.CacheReadTokens,
142 CacheWriteTokens: u.CacheWriteTokens + v.CacheWriteTokens,
143 CostUSD: u.CostUSD + v.CostUSD,
144 }
145}
146
147// StopReason describes why a turn ended.
148type StopReason string
149
150const (
151 StopEnd StopReason = "end"
152 StopLength StopReason = "length"
153 StopToolUse StopReason = "tool_use"
154 StopError StopReason = "error"
155 StopAborted StopReason = "aborted"
156)
157
158// Event is one item from a provider stream.
159type Event interface {
160 isEvent()
161}
162
163type EventStart struct {
164 Model string
165 Provider string
166}
167
168func (EventStart) isEvent() {}
169
170type EventTextDelta struct {
171 Delta string
172}
173
174func (EventTextDelta) isEvent() {}
175
176type EventToolStart struct {
177 ID string
178 Name string
179}
180
181func (EventToolStart) isEvent() {}
182
183type EventToolArgs struct {
184 ID string
185 Delta string // partial JSON
186}
187
188func (EventToolArgs) isEvent() {}
189
190type EventToolEnd struct {
191 ID string
192}
193
194func (EventToolEnd) isEvent() {}
195
196type EventUsage struct {
197 Usage Usage
198}
199
200func (EventUsage) isEvent() {}
201
202// EventDone is always the final event on a stream.
203type EventDone struct {
204 Stop StopReason
205 Err error
206 Message Message // fully assembled assistant message
207}
208
209func (EventDone) isEvent() {}
210
211// Request is a single LLM call.
212type Request struct {
213 Model string
214 System string
215 Messages []Message
216 Tools []Tool
217 MaxTokens int
218 Temperature *float32
219 // Reasoning is "", "minimum", "low", "medium", "high", or "maximum".
220 // Empty disables reasoning. Budget-based providers map these to roughly
221 // 1k/2k/8k/16k/32k thinking tokens; effort-based providers map them onto
222 // their closest supported reasoning_effort values.
223 Reasoning string
224}
225
226// Client is an LLM streaming client.
227type Client interface {
228 // Name returns "anthropic" or "openai".
229 Name() string
230 // Stream starts a request. The returned channel delivers events
231 // and is closed after EventDone. Errors during request setup are
232 // returned directly; runtime errors arrive as EventDone{Err: ...}.
233 Stream(ctx context.Context, req Request) (<-chan Event, error)
234}