blob: 1e53ea38a1313405d3c0c20a132bc8fff9d3ff87 [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.
78 Run func(ctx context.Context, input json.RawMessage) (string, error) `json:"-"`
79}
80
81type Content struct {
82 ID string
83 Type ContentType
84 Text string
85
86 // for thinking
87 Thinking string
88 Data string
89 Signature string
90
91 // for tool_use
92 ToolName string
93 ToolInput json.RawMessage
94
95 // for tool_result
96 ToolUseID string
97 ToolError bool
98 ToolResult string
99
100 // timing information for tool_result; added externally; not sent to the LLM
101 ToolUseStartTime *time.Time
102 ToolUseEndTime *time.Time
103
104 Cache bool
105}
106
107func StringContent(s string) Content {
108 return Content{Type: ContentTypeText, Text: s}
109}
110
111// ContentsAttr returns contents as a slog.Attr.
112// It is meant for logging.
113func ContentsAttr(contents []Content) slog.Attr {
114 var contentAttrs []any // slog.Attr
115 for _, content := range contents {
116 var attrs []any // slog.Attr
117 switch content.Type {
118 case ContentTypeText:
119 attrs = append(attrs, slog.String("text", content.Text))
120 case ContentTypeToolUse:
121 attrs = append(attrs, slog.String("tool_name", content.ToolName))
122 attrs = append(attrs, slog.String("tool_input", string(content.ToolInput)))
123 case ContentTypeToolResult:
124 attrs = append(attrs, slog.String("tool_result", content.ToolResult))
125 attrs = append(attrs, slog.Bool("tool_error", content.ToolError))
126 case ContentTypeThinking:
127 attrs = append(attrs, slog.String("thinking", content.Text))
128 default:
129 attrs = append(attrs, slog.String("unknown_content_type", content.Type.String()))
130 attrs = append(attrs, slog.Any("text", content)) // just log it all raw, better to have too much than not enough
131 }
132 contentAttrs = append(contentAttrs, slog.Group(content.ID, attrs...))
133 }
134 return slog.Group("contents", contentAttrs...)
135}
136
137type (
138 MessageRole int
139 ContentType int
140 ToolChoiceType int
141 StopReason int
142)
143
144//go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageRole,ContentType,ToolChoiceType,StopReason -output=llm_string.go
145
146const (
147 MessageRoleUser MessageRole = iota
148 MessageRoleAssistant
149
150 ContentTypeText ContentType = iota
151 ContentTypeThinking
152 ContentTypeRedactedThinking
153 ContentTypeToolUse
154 ContentTypeToolResult
155
156 ToolChoiceTypeAuto ToolChoiceType = iota // default
157 ToolChoiceTypeAny // any tool, but must use one
158 ToolChoiceTypeNone // no tools allowed
159 ToolChoiceTypeTool // must use the tool specified in the Name field
160
161 StopReasonStopSequence StopReason = iota
162 StopReasonMaxTokens
163 StopReasonEndTurn
164 StopReasonToolUse
165)
166
167type Response struct {
168 ID string
169 Type string
170 Role MessageRole
171 Model string
172 Content []Content
173 StopReason StopReason
174 StopSequence *string
175 Usage Usage
176 StartTime *time.Time
177 EndTime *time.Time
178}
179
180func (m *Response) ToMessage() Message {
181 return Message{
182 Role: m.Role,
183 Content: m.Content,
184 }
185}
186
187// Usage represents the billing and rate-limit usage.
188// Most LLM structs do not have JSON tags, to avoid accidental direct use in specific providers.
189// However, the front-end uses this struct, and it relies on its JSON serialization.
190// Do NOT use this struct directly when implementing an llm.Service.
191type Usage struct {
192 InputTokens uint64 `json:"input_tokens"`
193 CacheCreationInputTokens uint64 `json:"cache_creation_input_tokens"`
194 CacheReadInputTokens uint64 `json:"cache_read_input_tokens"`
195 OutputTokens uint64 `json:"output_tokens"`
196 CostUSD float64 `json:"cost_usd"`
197}
198
199func (u *Usage) Add(other Usage) {
200 u.InputTokens += other.InputTokens
201 u.CacheCreationInputTokens += other.CacheCreationInputTokens
202 u.CacheReadInputTokens += other.CacheReadInputTokens
203 u.OutputTokens += other.OutputTokens
204 u.CostUSD += other.CostUSD
205}
206
207func (u *Usage) String() string {
208 return fmt.Sprintf("in: %d, out: %d", u.InputTokens, u.OutputTokens)
209}
210
211func (u *Usage) IsZero() bool {
212 return *u == Usage{}
213}
214
215func (u *Usage) Attr() slog.Attr {
216 return slog.Group("usage",
217 slog.Uint64("input_tokens", u.InputTokens),
218 slog.Uint64("output_tokens", u.OutputTokens),
219 slog.Uint64("cache_creation_input_tokens", u.CacheCreationInputTokens),
220 slog.Uint64("cache_read_input_tokens", u.CacheReadInputTokens),
221 slog.Float64("cost_usd", u.CostUSD),
222 )
223}
224
225// UserStringMessage creates a user message with a single text content item.
226func UserStringMessage(text string) Message {
227 return Message{
228 Role: MessageRoleUser,
229 Content: []Content{StringContent(text)},
230 }
231}