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