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