1package provider
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "strings"
12 "time"
13)
14
15const anthropicDefaultBaseURL = "https://api.anthropic.com"
16const anthropicAPIVersion = "2023-06-01"
17
18// Stealth identity used when talking to Anthropic via subscription OAuth.
19// These values mimic the official Claude Code CLI so Anthropic's edge
20// accepts the request; diverging from them causes 429 rate_limit_error
21// or 403 on the very first request.
22const (
23 claudeCodeVersion = "2.1.75"
24 claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude."
25)
26
27// Claude Code's canonical tool casing. When running under OAuth we must
28// advertise tool names that match this list (case-insensitive lookup),
29// because Anthropic's backend cross-checks them.
30var claudeCodeToolNames = map[string]string{
31 "read": "Read",
32 "write": "Write",
33 "edit": "Edit",
34 "bash": "Bash",
35 "grep": "Grep",
36 "glob": "Glob",
37}
38
39func toClaudeCodeToolName(name string) string {
40 if cc, ok := claudeCodeToolNames[strings.ToLower(name)]; ok {
41 return cc
42 }
43 return name
44}
45
46func fromClaudeCodeToolName(name string, tools []Tool) string {
47 lower := strings.ToLower(name)
48 for _, t := range tools {
49 if strings.ToLower(t.Name) == lower {
50 return t.Name
51 }
52 }
53 return name
54}
55
56// anthropicClient implements Client against the Anthropic Messages API.
57type anthropicClient struct {
58 apiKey string
59 baseURL string
60 oauthTok string // when non-empty, send Bearer auth instead of x-api-key
61 http *http.Client
62}
63
64// NewAnthropic creates an Anthropic client using an API key. baseURL may be empty.
65func NewAnthropic(apiKey, baseURL string) Client {
66 if baseURL == "" {
67 baseURL = anthropicDefaultBaseURL
68 }
69 return &anthropicClient{
70 apiKey: apiKey,
71 baseURL: strings.TrimRight(baseURL, "/"),
72 http: &http.Client{Timeout: 0},
73 }
74}
75
76// NewAnthropicOAuth creates an Anthropic client using a subscription OAuth access token.
77func NewAnthropicOAuth(accessToken, baseURL string) Client {
78 if baseURL == "" {
79 baseURL = anthropicDefaultBaseURL
80 }
81 return &anthropicClient{
82 oauthTok: accessToken,
83 baseURL: strings.TrimRight(baseURL, "/"),
84 http: &http.Client{Timeout: 0},
85 }
86}
87
88func (c *anthropicClient) Name() string { return "anthropic" }
89
90// ---- wire types ----
91
92type anthTextBlock struct {
93 Type string `json:"type"` // "text"
94 Text string `json:"text"`
95 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
96}
97
98type anthImageSource struct {
99 Type string `json:"type"` // "base64"
100 MediaType string `json:"media_type"`
101 Data string `json:"data"`
102}
103
104type anthImageBlock struct {
105 Type string `json:"type"` // "image"
106 Source anthImageSource `json:"source"`
107}
108
109type anthToolUseBlock struct {
110 Type string `json:"type"` // "tool_use"
111 ID string `json:"id"`
112 Name string `json:"name"`
113 Input json.RawMessage `json:"input"`
114}
115
116type anthToolResultBlock struct {
117 Type string `json:"type"` // "tool_result"
118 ToolUseID string `json:"tool_use_id"`
119 Content json.RawMessage `json:"content"` // string or array of blocks
120 IsError bool `json:"is_error,omitempty"`
121}
122
123type anthCacheCtrl struct {
124 Type string `json:"type"` // "ephemeral"
125 TTL string `json:"ttl,omitempty"`
126}
127
128type anthMessage struct {
129 Role string `json:"role"`
130 Content []interface{} `json:"content"`
131}
132
133type anthSystemBlock struct {
134 Type string `json:"type"` // "text"
135 Text string `json:"text"`
136 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
137}
138
139type anthTool struct {
140 Name string `json:"name"`
141 Description string `json:"description,omitempty"`
142 InputSchema json.RawMessage `json:"input_schema"`
143 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
144}
145
146type anthThinking struct {
147 Type string `json:"type"` // "enabled"
148 BudgetTokens int `json:"budget_tokens"`
149}
150
151type anthRequest struct {
152 Model string `json:"model"`
153 MaxTokens int `json:"max_tokens"`
154 System []anthSystemBlock `json:"system,omitempty"`
155 Messages []anthMessage `json:"messages"`
156 Tools []anthTool `json:"tools,omitempty"`
157 Temperature *float32 `json:"temperature,omitempty"`
158 Thinking *anthThinking `json:"thinking,omitempty"`
159 Stream bool `json:"stream"`
160}
161
162// ---- request building ----
163
164func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) {
165 m, err := FindModel("anthropic", req.Model)
166 if err != nil {
167 return nil, err
168 }
169 maxTok := req.MaxTokens
170 if maxTok <= 0 {
171 maxTok = m.MaxOutput
172 }
173
174 out := &anthRequest{
175 Model: req.Model,
176 MaxTokens: maxTok,
177 Temperature: req.Temperature,
178 Stream: true,
179 }
180
181 // System prompt assembly differs between api-key and OAuth modes.
182 // OAuth requests MUST begin with the Claude Code identity line or
183 // Anthropic rejects them (429 rate_limit_error with zero tokens used).
184 if c.oauthTok != "" {
185 out.System = []anthSystemBlock{{
186 Type: "text",
187 Text: claudeCodeIdentity,
188 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
189 }}
190 if req.System != "" {
191 out.System = append(out.System, anthSystemBlock{
192 Type: "text",
193 Text: req.System,
194 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
195 })
196 }
197 } else if req.System != "" {
198 out.System = []anthSystemBlock{{
199 Type: "text",
200 Text: req.System,
201 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
202 }}
203 }
204
205 if req.Reasoning != "" && m.Reasoning {
206 budget := anthropicReasoningBudget(req.Reasoning)
207 if budget > 0 {
208 out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget}
209 // Reasoning requires max_tokens > budget.
210 if out.MaxTokens <= budget {
211 out.MaxTokens = budget + 4096
212 }
213 }
214 }
215
216 for _, t := range req.Tools {
217 name := t.Name
218 if c.oauthTok != "" {
219 name = toClaudeCodeToolName(name)
220 }
221 out.Tools = append(out.Tools, anthTool{
222 Name: name,
223 Description: t.Description,
224 InputSchema: t.Schema,
225 })
226 }
227 // Cache the last tool definition (applies cache breakpoint to the whole tools array).
228 if n := len(out.Tools); n > 0 {
229 out.Tools[n-1].CacheControl = &anthCacheCtrl{Type: "ephemeral"}
230 }
231
232 // Group messages: consecutive user/tool roles into one "user" message.
233 // Anthropic only has roles "user" and "assistant"; tool_result blocks live in user messages.
234 for _, msg := range req.Messages {
235 renameTools := c.oauthTok != ""
236 switch msg.Role {
237 case RoleUser:
238 out.Messages = append(out.Messages, anthMessage{
239 Role: "user",
240 Content: convertAnthContent(msg.Content, renameTools),
241 })
242 case RoleTool:
243 // Attach tool_result blocks to a user message; merge with prior user msg if last.
244 blocks := convertAnthContent(msg.Content, renameTools)
245 if n := len(out.Messages); n > 0 && out.Messages[n-1].Role == "user" {
246 out.Messages[n-1].Content = append(out.Messages[n-1].Content, blocks...)
247 } else {
248 out.Messages = append(out.Messages, anthMessage{Role: "user", Content: blocks})
249 }
250 case RoleAssistant:
251 out.Messages = append(out.Messages, anthMessage{
252 Role: "assistant",
253 Content: convertAnthContent(msg.Content, renameTools),
254 })
255 }
256 }
257
258 return out, nil
259}
260
261func anthropicReasoningBudget(level string) int {
262 switch strings.ToLower(level) {
263 case "low":
264 return 2048
265 case "medium":
266 return 8192
267 case "high":
268 return 16384
269 default:
270 return 0
271 }
272}
273
274func convertAnthContent(blocks []Content, renameTools bool) []interface{} {
275 out := make([]interface{}, 0, len(blocks))
276 for _, b := range blocks {
277 switch v := b.(type) {
278 case TextBlock:
279 if v.Text == "" {
280 continue
281 }
282 out = append(out, anthTextBlock{Type: "text", Text: v.Text})
283 case ImageBlock:
284 out = append(out, anthImageBlock{
285 Type: "image",
286 Source: anthImageSource{
287 Type: "base64",
288 MediaType: v.MimeType,
289 Data: base64.StdEncoding.EncodeToString(v.Data),
290 },
291 })
292 case ToolCallBlock:
293 args := v.Arguments
294 if len(args) == 0 {
295 args = json.RawMessage("{}")
296 }
297 name := v.Name
298 if renameTools {
299 name = toClaudeCodeToolName(name)
300 }
301 out = append(out, anthToolUseBlock{
302 Type: "tool_use", ID: v.ID, Name: name, Input: args,
303 })
304 case ToolResultBlock:
305 // Flatten content to a string if all text; else array of blocks.
306 content, _ := anthBuildToolResultContent(v.Content)
307 out = append(out, anthToolResultBlock{
308 Type: "tool_result",
309 ToolUseID: v.CallID,
310 Content: content,
311 IsError: v.IsError,
312 })
313 }
314 }
315 return out
316}
317
318func anthBuildToolResultContent(blocks []Content) (json.RawMessage, error) {
319 onlyText := true
320 var sb strings.Builder
321 for _, b := range blocks {
322 if tb, ok := b.(TextBlock); ok {
323 if sb.Len() > 0 {
324 sb.WriteString("\n")
325 }
326 sb.WriteString(tb.Text)
327 } else {
328 onlyText = false
329 break
330 }
331 }
332 if onlyText {
333 if sb.Len() == 0 {
334 return json.Marshal("")
335 }
336 return json.Marshal(sb.String())
337 }
338 // Array form: text + image blocks.
339 arr := make([]interface{}, 0, len(blocks))
340 for _, b := range blocks {
341 switch v := b.(type) {
342 case TextBlock:
343 arr = append(arr, anthTextBlock{Type: "text", Text: v.Text})
344 case ImageBlock:
345 arr = append(arr, anthImageBlock{
346 Type: "image",
347 Source: anthImageSource{
348 Type: "base64",
349 MediaType: v.MimeType,
350 Data: base64.StdEncoding.EncodeToString(v.Data),
351 },
352 })
353 }
354 }
355 return json.Marshal(arr)
356}
357
358// ---- streaming ----
359
360func (c *anthropicClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
361 wire, err := c.buildRequest(req)
362 if err != nil {
363 return nil, err
364 }
365 body, err := json.Marshal(wire)
366 if err != nil {
367 return nil, err
368 }
369
370 httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/v1/messages", bytes.NewReader(body))
371 if err != nil {
372 return nil, err
373 }
374 httpReq.Header.Set("content-type", "application/json")
375 httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
376 if c.oauthTok != "" {
377 // Claude-Code-shaped request: identical headers and values as the
378 // official CLI. Any drift triggers Anthropic's anti-abuse check and
379 // rate-limits (or outright blocks) the request.
380 httpReq.Header.Set("accept", "application/json")
381 httpReq.Header.Set("authorization", "Bearer "+c.oauthTok)
382 httpReq.Header.Set("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14")
383 httpReq.Header.Set("anthropic-dangerous-direct-browser-access", "true")
384 httpReq.Header.Set("user-agent", "claude-cli/"+claudeCodeVersion+" (external, cli)")
385 httpReq.Header.Set("x-app", "cli")
386 // Remove x-api-key entirely by NOT setting it.
387 } else {
388 httpReq.Header.Set("accept", "text/event-stream")
389 httpReq.Header.Set("x-api-key", c.apiKey)
390 }
391
392 resp, err := c.http.Do(httpReq)
393 if err != nil {
394 return nil, fmt.Errorf("anthropic: %w", err)
395 }
396 if resp.StatusCode != http.StatusOK {
397 b, _ := io.ReadAll(resp.Body)
398 resp.Body.Close()
399 return nil, fmt.Errorf("anthropic: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
400 }
401
402 out := make(chan Event, 16)
403 go c.runStream(ctx, resp, req, out)
404 return out, nil
405}
406
407func (c *anthropicClient) runStream(ctx context.Context, resp *http.Response, req Request, out chan<- Event) {
408 defer close(out)
409 defer resp.Body.Close()
410
411 model, _ := FindModel("anthropic", req.Model)
412 out <- EventStart{Model: req.Model, Provider: "anthropic"}
413
414 raw := make(chan sseEvent, 16)
415 go readSSE(resp.Body, raw)
416
417 // State for assembling the assistant message. Blocks are indexed
418 // by their `index` field from Anthropic so we can preserve the
419 // interleaved order the model emitted them in (text may come
420 // before OR after tool_use; mixing both happens frequently).
421 type blockEntry struct {
422 kind string // "text" | "tool_use"
423 textBuf strings.Builder
424 toolCall ToolCallBlock
425 toolArgs strings.Builder
426 }
427 var (
428 blocks = map[int]*blockEntry{}
429 blockOrder []int // insertion order of indexes
430 activeIdx = -1
431 usage Usage
432 stop StopReason = StopEnd
433 finalErr error
434 )
435 _ = activeIdx // read-only indicator used for legacy parity
436
437 registerBlock := func(idx int, kind string) *blockEntry {
438 if be, ok := blocks[idx]; ok {
439 return be
440 }
441 be := &blockEntry{kind: kind}
442 blocks[idx] = be
443 blockOrder = append(blockOrder, idx)
444 return be
445 }
446
447 assembleMsg := func() Message {
448 content := []Content{}
449 for _, idx := range blockOrder {
450 be := blocks[idx]
451 switch be.kind {
452 case "text":
453 if be.textBuf.Len() > 0 {
454 content = append(content, TextBlock{Text: be.textBuf.String()})
455 }
456 case "tool_use":
457 tc := be.toolCall
458 args := be.toolArgs.String()
459 if args == "" {
460 args = "{}"
461 }
462 tc.Arguments = json.RawMessage(args)
463 content = append(content, tc)
464 }
465 }
466 return Message{Role: RoleAssistant, Content: content, Time: time.Now()}
467 }
468
469 sendDone := func() {
470 usage.CostUSD = ComputeCost(model, usage)
471 out <- EventUsage{Usage: usage}
472 out <- EventDone{Stop: stop, Err: finalErr, Message: assembleMsg()}
473 }
474
475 for {
476 select {
477 case <-ctx.Done():
478 stop = StopAborted
479 finalErr = ctx.Err()
480 sendDone()
481 return
482 case ev, ok := <-raw:
483 if !ok {
484 sendDone()
485 return
486 }
487 // Parse event payload based on event: type.
488 var payload map[string]json.RawMessage
489 if err := json.Unmarshal([]byte(ev.Data), &payload); err != nil {
490 continue
491 }
492 switch ev.Event {
493 case "content_block_start":
494 var idx int
495 if b, ok := payload["index"]; ok {
496 _ = json.Unmarshal(b, &idx)
497 }
498 var block struct {
499 Type string `json:"type"`
500 ID string `json:"id"`
501 Name string `json:"name"`
502 Text string `json:"text"`
503 Input json.RawMessage `json:"input"`
504 }
505 if b, ok := payload["content_block"]; ok {
506 _ = json.Unmarshal(b, &block)
507 }
508 activeIdx = idx
509 switch block.Type {
510 case "tool_use":
511 name := block.Name
512 if c.oauthTok != "" {
513 name = fromClaudeCodeToolName(name, req.Tools)
514 }
515 be := registerBlock(idx, "tool_use")
516 be.toolCall = ToolCallBlock{ID: block.ID, Name: name}
517 out <- EventToolStart{ID: block.ID, Name: name}
518 case "text":
519 registerBlock(idx, "text")
520 case "thinking":
521 // not surfaced
522 }
523 case "content_block_delta":
524 var idx int
525 if b, ok := payload["index"]; ok {
526 _ = json.Unmarshal(b, &idx)
527 }
528 var d struct {
529 Type string `json:"type"`
530 Text string `json:"text"`
531 PartialJSON string `json:"partial_json"`
532 Thinking string `json:"thinking"`
533 }
534 if b, ok := payload["delta"]; ok {
535 _ = json.Unmarshal(b, &d)
536 }
537 switch d.Type {
538 case "text_delta":
539 if be, ok := blocks[idx]; ok && be.kind == "text" {
540 be.textBuf.WriteString(d.Text)
541 }
542 out <- EventTextDelta{Delta: d.Text}
543 case "input_json_delta":
544 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
545 be.toolArgs.WriteString(d.PartialJSON)
546 out <- EventToolArgs{ID: be.toolCall.ID, Delta: d.PartialJSON}
547 }
548 case "thinking_delta":
549 // Not surfaced in v1.
550 }
551 case "content_block_stop":
552 var idx int
553 if b, ok := payload["index"]; ok {
554 _ = json.Unmarshal(b, &idx)
555 }
556 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
557 out <- EventToolEnd{ID: be.toolCall.ID}
558 }
559 activeIdx = -1
560 case "message_start":
561 var m struct {
562 Message struct {
563 Usage struct {
564 InputTokens int `json:"input_tokens"`
565 OutputTokens int `json:"output_tokens"`
566 CacheReadInputTokens int `json:"cache_read_input_tokens"`
567 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
568 } `json:"usage"`
569 } `json:"message"`
570 }
571 _ = json.Unmarshal([]byte(ev.Data), &m)
572 usage.InputTokens += m.Message.Usage.InputTokens
573 usage.OutputTokens += m.Message.Usage.OutputTokens
574 usage.CacheReadTokens += m.Message.Usage.CacheReadInputTokens
575 usage.CacheWriteTokens += m.Message.Usage.CacheCreationInputTokens
576 case "message_delta":
577 var m struct {
578 Delta struct {
579 StopReason string `json:"stop_reason"`
580 } `json:"delta"`
581 Usage struct {
582 InputTokens int `json:"input_tokens"`
583 OutputTokens int `json:"output_tokens"`
584 CacheReadInputTokens int `json:"cache_read_input_tokens"`
585 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
586 } `json:"usage"`
587 }
588 _ = json.Unmarshal([]byte(ev.Data), &m)
589 usage.InputTokens += m.Usage.InputTokens
590 usage.OutputTokens += m.Usage.OutputTokens
591 usage.CacheReadTokens += m.Usage.CacheReadInputTokens
592 usage.CacheWriteTokens += m.Usage.CacheCreationInputTokens
593 switch m.Delta.StopReason {
594 case "end_turn", "stop_sequence":
595 stop = StopEnd
596 case "max_tokens":
597 stop = StopLength
598 case "tool_use":
599 stop = StopToolUse
600 }
601 case "message_stop":
602 sendDone()
603 return
604 case "error":
605 var e struct {
606 Error struct {
607 Type string `json:"type"`
608 Message string `json:"message"`
609 } `json:"error"`
610 }
611 _ = json.Unmarshal([]byte(ev.Data), &e)
612 stop = StopError
613 finalErr = fmt.Errorf("anthropic %s: %s", e.Error.Type, e.Error.Message)
614 sendDone()
615 return
616 }
617 _ = activeIdx
618 }
619 }
620}