v0.2.88 24f1547

provider.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close