blob: 97189540914e5f2ebfb0b5185bf02787fc6ad75e [file] [log] [blame]
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -07001// Package llm provides a unified interface for interacting with LLMs.
2package llm
3
4import (
5 "context"
6 "encoding/json"
7 "fmt"
8 "log/slog"
Josh Bleecher Snyder59bb27d2025-06-05 07:32:10 -07009 "net/http"
10 "strconv"
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070011 "strings"
12 "time"
13)
14
15type Service interface {
16 // Do sends a request to an LLM.
17 Do(context.Context, *Request) (*Response, error)
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070018 // TokenContextWindow returns the maximum token context window size for this service
19 TokenContextWindow() int
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070020}
21
22// MustSchema validates that schema is a valid JSON schema and returns it as a json.RawMessage.
23// It panics if the schema is invalid.
24func MustSchema(schema string) json.RawMessage {
25 // TODO: validate schema, for now just make sure it's valid JSON
26 schema = strings.TrimSpace(schema)
27 bytes := []byte(schema)
28 if !json.Valid(bytes) {
29 panic("invalid JSON schema: " + schema)
30 }
31 return json.RawMessage(bytes)
32}
33
Josh Bleecher Snyder74d690e2025-05-14 18:16:03 -070034func EmptySchema() json.RawMessage {
35 return MustSchema(`{"type": "object", "properties": {}}`)
36}
37
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070038type Request struct {
39 Messages []Message
40 ToolChoice *ToolChoice
41 Tools []*Tool
42 System []SystemContent
43}
44
45// Message represents a message in the conversation.
46type Message struct {
47 Role MessageRole
48 Content []Content
49 ToolUse *ToolUse // use to control whether/which tool to use
50}
51
52// ToolUse represents a tool use in the message content.
53type ToolUse struct {
54 ID string
55 Name string
56}
57
58type ToolChoice struct {
59 Type ToolChoiceType
60 Name string
61}
62
63type SystemContent struct {
64 Text string
65 Type string
66 Cache bool
67}
68
69// Tool represents a tool available to an LLM.
70type Tool struct {
71 Name string
72 // Type is used by the text editor tool; see
73 // https://docs.anthropic.com/en/docs/build-with-claude/tool-use/text-editor-tool
74 Type string
75 Description string
76 InputSchema json.RawMessage
Sean McCullough021557a2025-05-05 23:20:53 +000077 // EndsTurn indicates that this tool should cause the model to end its turn when used
78 EndsTurn bool
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070079
80 // The Run function is automatically called when the tool is used.
81 // Run functions may be called concurrently with each other and themselves.
82 // The input to Run function is the input to the tool, as provided by Claude, in compliance with the input schema.
83 // The outputs from Run will be sent back to Claude.
84 // If you do not want to respond to the tool call request from Claude, return ErrDoNotRespond.
85 // ctx contains extra (rarely used) tool call information; retrieve it with ToolCallInfoFromContext.
Philip Zeyliger72252cb2025-05-10 17:00:08 -070086 Run func(ctx context.Context, input json.RawMessage) ([]Content, error) `json:"-"`
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070087}
88
89type Content struct {
90 ID string
91 Type ContentType
92 Text string
93
Philip Zeyliger72252cb2025-05-10 17:00:08 -070094 // Media type for image content
95 MediaType string
96
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070097 // for thinking
98 Thinking string
99 Data string
100 Signature string
101
102 // for tool_use
103 ToolName string
104 ToolInput json.RawMessage
105
106 // for tool_result
107 ToolUseID string
108 ToolError bool
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700109 ToolResult []Content
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -0700110
111 // timing information for tool_result; added externally; not sent to the LLM
112 ToolUseStartTime *time.Time
113 ToolUseEndTime *time.Time
114
115 Cache bool
116}
117
118func StringContent(s string) Content {
119 return Content{Type: ContentTypeText, Text: s}
120}
121
122// ContentsAttr returns contents as a slog.Attr.
123// It is meant for logging.
124func ContentsAttr(contents []Content) slog.Attr {
125 var contentAttrs []any // slog.Attr
126 for _, content := range contents {
127 var attrs []any // slog.Attr
128 switch content.Type {
129 case ContentTypeText:
130 attrs = append(attrs, slog.String("text", content.Text))
131 case ContentTypeToolUse:
132 attrs = append(attrs, slog.String("tool_name", content.ToolName))
133 attrs = append(attrs, slog.String("tool_input", string(content.ToolInput)))
134 case ContentTypeToolResult:
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700135 attrs = append(attrs, slog.Any("tool_result", content.ToolResult))
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -0700136 attrs = append(attrs, slog.Bool("tool_error", content.ToolError))
137 case ContentTypeThinking:
138 attrs = append(attrs, slog.String("thinking", content.Text))
139 default:
140 attrs = append(attrs, slog.String("unknown_content_type", content.Type.String()))
141 attrs = append(attrs, slog.Any("text", content)) // just log it all raw, better to have too much than not enough
142 }
143 contentAttrs = append(contentAttrs, slog.Group(content.ID, attrs...))
144 }
145 return slog.Group("contents", contentAttrs...)
146}
147
148type (
149 MessageRole int
150 ContentType int
151 ToolChoiceType int
152 StopReason int
153)
154
155//go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageRole,ContentType,ToolChoiceType,StopReason -output=llm_string.go
156
157const (
158 MessageRoleUser MessageRole = iota
159 MessageRoleAssistant
160
161 ContentTypeText ContentType = iota
162 ContentTypeThinking
163 ContentTypeRedactedThinking
164 ContentTypeToolUse
165 ContentTypeToolResult
166
167 ToolChoiceTypeAuto ToolChoiceType = iota // default
168 ToolChoiceTypeAny // any tool, but must use one
169 ToolChoiceTypeNone // no tools allowed
170 ToolChoiceTypeTool // must use the tool specified in the Name field
171
172 StopReasonStopSequence StopReason = iota
173 StopReasonMaxTokens
174 StopReasonEndTurn
175 StopReasonToolUse
Josh Bleecher Snyder0e8073a2025-05-22 21:04:51 -0700176 StopReasonRefusal
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -0700177)
178
179type Response struct {
180 ID string
181 Type string
182 Role MessageRole
183 Model string
184 Content []Content
185 StopReason StopReason
186 StopSequence *string
187 Usage Usage
188 StartTime *time.Time
189 EndTime *time.Time
190}
191
192func (m *Response) ToMessage() Message {
193 return Message{
194 Role: m.Role,
195 Content: m.Content,
196 }
197}
198
Josh Bleecher Snyder59bb27d2025-06-05 07:32:10 -0700199func CostUSDFromResponse(headers http.Header) float64 {
200 h := headers.Get("Skaband-Cost-Microcents")
201 if h == "" {
202 return 0
203 }
204 uc, err := strconv.ParseUint(h, 10, 64)
205 if err != nil {
206 slog.Warn("failed to parse cost header", "header", h)
207 return 0
208 }
209 return float64(uc) / 100_000_000
210}
211
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -0700212// Usage represents the billing and rate-limit usage.
213// Most LLM structs do not have JSON tags, to avoid accidental direct use in specific providers.
214// However, the front-end uses this struct, and it relies on its JSON serialization.
215// Do NOT use this struct directly when implementing an llm.Service.
216type Usage struct {
217 InputTokens uint64 `json:"input_tokens"`
218 CacheCreationInputTokens uint64 `json:"cache_creation_input_tokens"`
219 CacheReadInputTokens uint64 `json:"cache_read_input_tokens"`
220 OutputTokens uint64 `json:"output_tokens"`
221 CostUSD float64 `json:"cost_usd"`
222}
223
224func (u *Usage) Add(other Usage) {
225 u.InputTokens += other.InputTokens
226 u.CacheCreationInputTokens += other.CacheCreationInputTokens
227 u.CacheReadInputTokens += other.CacheReadInputTokens
228 u.OutputTokens += other.OutputTokens
229 u.CostUSD += other.CostUSD
230}
231
232func (u *Usage) String() string {
233 return fmt.Sprintf("in: %d, out: %d", u.InputTokens, u.OutputTokens)
234}
235
236func (u *Usage) IsZero() bool {
237 return *u == Usage{}
238}
239
240func (u *Usage) Attr() slog.Attr {
241 return slog.Group("usage",
242 slog.Uint64("input_tokens", u.InputTokens),
243 slog.Uint64("output_tokens", u.OutputTokens),
244 slog.Uint64("cache_creation_input_tokens", u.CacheCreationInputTokens),
245 slog.Uint64("cache_read_input_tokens", u.CacheReadInputTokens),
246 slog.Float64("cost_usd", u.CostUSD),
247 )
248}
249
250// UserStringMessage creates a user message with a single text content item.
251func UserStringMessage(text string) Message {
252 return Message{
253 Role: MessageRoleUser,
254 Content: []Content{StringContent(text)},
255 }
256}
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700257
258// TextContent creates a simple text content for tool results.
259// This is a helper function to create the most common type of tool result content.
260func TextContent(text string) []Content {
261 return []Content{{
262 Type: ContentTypeText,
263 Text: text,
264 }}
265}
266
267// ImageContent creates an image content for tool results.
268// MediaType should be "image/jpeg" or "image/png"
269func ImageContent(text string, mediaType string, base64Data string) []Content {
270 return []Content{{
271 Type: ContentTypeText,
272 Text: text,
273 MediaType: mediaType,
274 Data: base64Data,
275 }}
276}