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