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 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
108}
109
110type anthToolUseBlock struct {
111 Type string `json:"type"` // "tool_use"
112 ID string `json:"id"`
113 Name string `json:"name"`
114 Input json.RawMessage `json:"input"`
115 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
116}
117
118type anthToolResultBlock struct {
119 Type string `json:"type"` // "tool_result"
120 ToolUseID string `json:"tool_use_id"`
121 Content json.RawMessage `json:"content"` // string or array of blocks
122 IsError bool `json:"is_error,omitempty"`
123 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
124}
125
126type anthCacheCtrl struct {
127 Type string `json:"type"` // "ephemeral"
128 TTL string `json:"ttl,omitempty"`
129}
130
131type anthMessage struct {
132 Role string `json:"role"`
133 Content []interface{} `json:"content"`
134}
135
136type anthSystemBlock struct {
137 Type string `json:"type"` // "text"
138 Text string `json:"text"`
139 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
140}
141
142type anthTool struct {
143 Name string `json:"name"`
144 Description string `json:"description,omitempty"`
145 InputSchema json.RawMessage `json:"input_schema"`
146 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
147}
148
149type anthThinking struct {
150 Type string `json:"type"` // "enabled"
151 BudgetTokens int `json:"budget_tokens"`
152}
153
154type anthRequest struct {
155 Model string `json:"model"`
156 MaxTokens int `json:"max_tokens"`
157 System []anthSystemBlock `json:"system,omitempty"`
158 Messages []anthMessage `json:"messages"`
159 Tools []anthTool `json:"tools,omitempty"`
160 Temperature *float32 `json:"temperature,omitempty"`
161 Thinking *anthThinking `json:"thinking,omitempty"`
162 Stream bool `json:"stream"`
163}
164
165// ---- request building ----
166
167func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) {
168 m, err := FindModel("anthropic", req.Model)
169 if err != nil {
170 return nil, err
171 }
172 maxTok := req.MaxTokens
173 if maxTok <= 0 {
174 maxTok = m.MaxOutput
175 }
176
177 out := &anthRequest{
178 Model: req.Model,
179 MaxTokens: maxTok,
180 Temperature: req.Temperature,
181 Stream: true,
182 }
183
184 // System prompt assembly differs between api-key and OAuth modes.
185 // OAuth requests MUST begin with the Claude Code identity line or
186 // Anthropic rejects them (429 rate_limit_error with zero tokens used).
187 //
188 // Cache budget: anthropic caps cache_control to 4 breakpoints per
189 // request. We spend them on (system prompt) + (tools tail) + (last
190 // two user messages). The claude-code identity line stays uncached
191 // because it's a few tokens and gets folded into the larger prefix
192 // implicitly anyway.
193 if c.oauthTok != "" {
194 out.System = []anthSystemBlock{{
195 Type: "text",
196 Text: claudeCodeIdentity,
197 }}
198 if req.System != "" {
199 out.System = append(out.System, anthSystemBlock{
200 Type: "text",
201 Text: req.System,
202 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
203 })
204 }
205 } else if req.System != "" {
206 out.System = []anthSystemBlock{{
207 Type: "text",
208 Text: req.System,
209 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
210 }}
211 }
212
213 if req.Reasoning != "" && m.Reasoning {
214 budget := anthropicReasoningBudget(req.Reasoning)
215 if budget > 0 {
216 out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget}
217 // Reasoning requires max_tokens > budget.
218 if out.MaxTokens <= budget {
219 out.MaxTokens = budget + 4096
220 }
221 }
222 }
223
224 for _, t := range req.Tools {
225 name := t.Name
226 if c.oauthTok != "" {
227 name = toClaudeCodeToolName(name)
228 }
229 out.Tools = append(out.Tools, anthTool{
230 Name: name,
231 Description: t.Description,
232 InputSchema: t.Schema,
233 })
234 }
235 // Cache the last tool definition (applies cache breakpoint to the whole tools array).
236 if n := len(out.Tools); n > 0 {
237 out.Tools[n-1].CacheControl = &anthCacheCtrl{Type: "ephemeral"}
238 }
239
240 // Group messages: consecutive user/tool roles into one "user" message.
241 // Anthropic only has roles "user" and "assistant"; tool_result blocks live in user messages.
242 for _, msg := range req.Messages {
243 renameTools := c.oauthTok != ""
244 switch msg.Role {
245 case RoleUser:
246 out.Messages = append(out.Messages, anthMessage{
247 Role: "user",
248 Content: convertAnthContent(msg.Content, renameTools),
249 })
250 case RoleTool:
251 // Attach tool_result blocks to a user message; merge with prior user msg if last.
252 blocks := convertAnthContent(msg.Content, renameTools)
253 if n := len(out.Messages); n > 0 && out.Messages[n-1].Role == "user" {
254 out.Messages[n-1].Content = append(out.Messages[n-1].Content, blocks...)
255 } else {
256 out.Messages = append(out.Messages, anthMessage{Role: "user", Content: blocks})
257 }
258 case RoleAssistant:
259 out.Messages = append(out.Messages, anthMessage{
260 Role: "assistant",
261 Content: convertAnthContent(msg.Content, renameTools),
262 })
263 }
264 }
265
266 // Mark the last two user messages with cache_control so anthropic
267 // caches the running conversation prefix. Combined with the system
268 // + tools breakpoints above this is the recommended layout for
269 // multi-turn caching: turn N writes a cache that turn N+1 reads,
270 // dropping per-turn input tokens from "system + history" down to
271 // just the new user message. Anthropic allows up to 4 breakpoints
272 // per request; we use system + tools + 2 conversation = 4.
273 tagUserCache(out.Messages)
274
275 return out, nil
276}
277
278// tagUserCache attaches a cache_control marker to the last block of
279// the most recent (and second-most-recent, if any) user message in
280// msgs. The marker tells the api to checkpoint the prefix at that
281// point so subsequent requests can replay everything up to and
282// including that block as a cache hit.
283func tagUserCache(msgs []anthMessage) {
284 indexes := make([]int, 0, 2)
285 for i := len(msgs) - 1; i >= 0 && len(indexes) < 2; i-- {
286 if msgs[i].Role == "user" {
287 indexes = append(indexes, i)
288 }
289 }
290 for _, idx := range indexes {
291 markLastBlockEphemeral(msgs[idx].Content)
292 }
293}
294
295// markLastBlockEphemeral sets CacheControl on the last entry in blocks
296// regardless of whether it's a text, image, tool_use, or tool_result.
297// Each block type carries its own CacheControl pointer so we type-
298// switch + reassign the slice element.
299func markLastBlockEphemeral(blocks []interface{}) {
300 if len(blocks) == 0 {
301 return
302 }
303 i := len(blocks) - 1
304 cc := &anthCacheCtrl{Type: "ephemeral"}
305 switch v := blocks[i].(type) {
306 case anthTextBlock:
307 v.CacheControl = cc
308 blocks[i] = v
309 case anthImageBlock:
310 v.CacheControl = cc
311 blocks[i] = v
312 case anthToolUseBlock:
313 v.CacheControl = cc
314 blocks[i] = v
315 case anthToolResultBlock:
316 v.CacheControl = cc
317 blocks[i] = v
318 }
319}
320
321func anthropicReasoningBudget(level string) int {
322 switch strings.ToLower(level) {
323 case "low":
324 return 2048
325 case "medium":
326 return 8192
327 case "high":
328 return 16384
329 default:
330 return 0
331 }
332}
333
334func convertAnthContent(blocks []Content, renameTools bool) []interface{} {
335 out := make([]interface{}, 0, len(blocks))
336 for _, b := range blocks {
337 switch v := b.(type) {
338 case TextBlock:
339 if v.Text == "" {
340 continue
341 }
342 out = append(out, anthTextBlock{Type: "text", Text: v.Text})
343 case ImageBlock:
344 out = append(out, anthImageBlock{
345 Type: "image",
346 Source: anthImageSource{
347 Type: "base64",
348 MediaType: v.MimeType,
349 Data: base64.StdEncoding.EncodeToString(v.Data),
350 },
351 })
352 case ToolCallBlock:
353 args := v.Arguments
354 if len(args) == 0 {
355 args = json.RawMessage("{}")
356 }
357 name := v.Name
358 if renameTools {
359 name = toClaudeCodeToolName(name)
360 }
361 out = append(out, anthToolUseBlock{
362 Type: "tool_use", ID: v.ID, Name: name, Input: args,
363 })
364 case ToolResultBlock:
365 // Flatten content to a string if all text; else array of blocks.
366 content, _ := anthBuildToolResultContent(v.Content)
367 out = append(out, anthToolResultBlock{
368 Type: "tool_result",
369 ToolUseID: v.CallID,
370 Content: content,
371 IsError: v.IsError,
372 })
373 }
374 }
375 return out
376}
377
378func anthBuildToolResultContent(blocks []Content) (json.RawMessage, error) {
379 onlyText := true
380 var sb strings.Builder
381 for _, b := range blocks {
382 if tb, ok := b.(TextBlock); ok {
383 if sb.Len() > 0 {
384 sb.WriteString("\n")
385 }
386 sb.WriteString(tb.Text)
387 } else {
388 onlyText = false
389 break
390 }
391 }
392 if onlyText {
393 if sb.Len() == 0 {
394 return json.Marshal("")
395 }
396 return json.Marshal(sb.String())
397 }
398 // Array form: text + image blocks.
399 arr := make([]interface{}, 0, len(blocks))
400 for _, b := range blocks {
401 switch v := b.(type) {
402 case TextBlock:
403 arr = append(arr, anthTextBlock{Type: "text", Text: v.Text})
404 case ImageBlock:
405 arr = append(arr, anthImageBlock{
406 Type: "image",
407 Source: anthImageSource{
408 Type: "base64",
409 MediaType: v.MimeType,
410 Data: base64.StdEncoding.EncodeToString(v.Data),
411 },
412 })
413 }
414 }
415 return json.Marshal(arr)
416}
417
418// ---- streaming ----
419
420func (c *anthropicClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
421 wire, err := c.buildRequest(req)
422 if err != nil {
423 return nil, err
424 }
425 body, err := json.Marshal(wire)
426 if err != nil {
427 return nil, err
428 }
429
430 httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/v1/messages", bytes.NewReader(body))
431 if err != nil {
432 return nil, err
433 }
434 httpReq.Header.Set("content-type", "application/json")
435 httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
436 if c.oauthTok != "" {
437 // Claude-Code-shaped request: identical headers and values as the
438 // official CLI. Any drift triggers Anthropic's anti-abuse check and
439 // rate-limits (or outright blocks) the request.
440 httpReq.Header.Set("accept", "application/json")
441 httpReq.Header.Set("authorization", "Bearer "+c.oauthTok)
442 httpReq.Header.Set("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14")
443 httpReq.Header.Set("anthropic-dangerous-direct-browser-access", "true")
444 httpReq.Header.Set("user-agent", "claude-cli/"+claudeCodeVersion+" (external, cli)")
445 httpReq.Header.Set("x-app", "cli")
446 // Remove x-api-key entirely by NOT setting it.
447 } else {
448 httpReq.Header.Set("accept", "text/event-stream")
449 httpReq.Header.Set("x-api-key", c.apiKey)
450 }
451
452 resp, err := c.http.Do(httpReq)
453 if err != nil {
454 return nil, fmt.Errorf("anthropic: %w", err)
455 }
456 if resp.StatusCode != http.StatusOK {
457 b, _ := io.ReadAll(resp.Body)
458 resp.Body.Close()
459 return nil, fmt.Errorf("anthropic: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
460 }
461
462 out := make(chan Event, 16)
463 go c.runStream(ctx, resp, req, out)
464 return out, nil
465}
466
467func (c *anthropicClient) runStream(ctx context.Context, resp *http.Response, req Request, out chan<- Event) {
468 defer close(out)
469 defer resp.Body.Close()
470
471 model, _ := FindModel("anthropic", req.Model)
472 out <- EventStart{Model: req.Model, Provider: "anthropic"}
473
474 raw := make(chan sseEvent, 16)
475 go readSSE(resp.Body, raw)
476
477 // State for assembling the assistant message. Blocks are indexed
478 // by their `index` field from Anthropic so we can preserve the
479 // interleaved order the model emitted them in (text may come
480 // before OR after tool_use; mixing both happens frequently).
481 type blockEntry struct {
482 kind string // "text" | "tool_use"
483 textBuf strings.Builder
484 toolCall ToolCallBlock
485 toolArgs strings.Builder
486 }
487 var (
488 blocks = map[int]*blockEntry{}
489 blockOrder []int // insertion order of indexes
490 activeIdx = -1
491 usage Usage
492 stop StopReason = StopEnd
493 finalErr error
494 )
495 _ = activeIdx // read-only indicator used for legacy parity
496
497 registerBlock := func(idx int, kind string) *blockEntry {
498 if be, ok := blocks[idx]; ok {
499 return be
500 }
501 be := &blockEntry{kind: kind}
502 blocks[idx] = be
503 blockOrder = append(blockOrder, idx)
504 return be
505 }
506
507 assembleMsg := func() Message {
508 content := []Content{}
509 for _, idx := range blockOrder {
510 be := blocks[idx]
511 switch be.kind {
512 case "text":
513 if be.textBuf.Len() > 0 {
514 content = append(content, TextBlock{Text: be.textBuf.String()})
515 }
516 case "tool_use":
517 tc := be.toolCall
518 args := be.toolArgs.String()
519 if args == "" {
520 args = "{}"
521 }
522 tc.Arguments = json.RawMessage(args)
523 content = append(content, tc)
524 }
525 }
526 return Message{Role: RoleAssistant, Content: content, Time: time.Now()}
527 }
528
529 sendDone := func() {
530 usage.CostUSD = ComputeCost(model, usage)
531 out <- EventUsage{Usage: usage}
532 out <- EventDone{Stop: stop, Err: finalErr, Message: assembleMsg()}
533 }
534
535 for {
536 select {
537 case <-ctx.Done():
538 stop = StopAborted
539 finalErr = ctx.Err()
540 sendDone()
541 return
542 case ev, ok := <-raw:
543 if !ok {
544 sendDone()
545 return
546 }
547 // Parse event payload based on event: type.
548 var payload map[string]json.RawMessage
549 if err := json.Unmarshal([]byte(ev.Data), &payload); err != nil {
550 continue
551 }
552 switch ev.Event {
553 case "content_block_start":
554 var idx int
555 if b, ok := payload["index"]; ok {
556 _ = json.Unmarshal(b, &idx)
557 }
558 var block struct {
559 Type string `json:"type"`
560 ID string `json:"id"`
561 Name string `json:"name"`
562 Text string `json:"text"`
563 Input json.RawMessage `json:"input"`
564 }
565 if b, ok := payload["content_block"]; ok {
566 _ = json.Unmarshal(b, &block)
567 }
568 activeIdx = idx
569 switch block.Type {
570 case "tool_use":
571 name := block.Name
572 if c.oauthTok != "" {
573 name = fromClaudeCodeToolName(name, req.Tools)
574 }
575 be := registerBlock(idx, "tool_use")
576 be.toolCall = ToolCallBlock{ID: block.ID, Name: name}
577 out <- EventToolStart{ID: block.ID, Name: name}
578 case "text":
579 registerBlock(idx, "text")
580 case "thinking":
581 // not surfaced
582 }
583 case "content_block_delta":
584 var idx int
585 if b, ok := payload["index"]; ok {
586 _ = json.Unmarshal(b, &idx)
587 }
588 var d struct {
589 Type string `json:"type"`
590 Text string `json:"text"`
591 PartialJSON string `json:"partial_json"`
592 Thinking string `json:"thinking"`
593 }
594 if b, ok := payload["delta"]; ok {
595 _ = json.Unmarshal(b, &d)
596 }
597 switch d.Type {
598 case "text_delta":
599 if be, ok := blocks[idx]; ok && be.kind == "text" {
600 be.textBuf.WriteString(d.Text)
601 }
602 out <- EventTextDelta{Delta: d.Text}
603 case "input_json_delta":
604 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
605 be.toolArgs.WriteString(d.PartialJSON)
606 out <- EventToolArgs{ID: be.toolCall.ID, Delta: d.PartialJSON}
607 }
608 case "thinking_delta":
609 // Not surfaced in v1.
610 }
611 case "content_block_stop":
612 var idx int
613 if b, ok := payload["index"]; ok {
614 _ = json.Unmarshal(b, &idx)
615 }
616 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
617 out <- EventToolEnd{ID: be.toolCall.ID}
618 }
619 activeIdx = -1
620 case "message_start":
621 var m struct {
622 Message struct {
623 Usage struct {
624 InputTokens int `json:"input_tokens"`
625 OutputTokens int `json:"output_tokens"`
626 CacheReadInputTokens int `json:"cache_read_input_tokens"`
627 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
628 } `json:"usage"`
629 } `json:"message"`
630 }
631 _ = json.Unmarshal([]byte(ev.Data), &m)
632 usage.InputTokens += m.Message.Usage.InputTokens
633 usage.OutputTokens += m.Message.Usage.OutputTokens
634 usage.CacheReadTokens += m.Message.Usage.CacheReadInputTokens
635 usage.CacheWriteTokens += m.Message.Usage.CacheCreationInputTokens
636 case "message_delta":
637 var m struct {
638 Delta struct {
639 StopReason string `json:"stop_reason"`
640 } `json:"delta"`
641 Usage struct {
642 InputTokens int `json:"input_tokens"`
643 OutputTokens int `json:"output_tokens"`
644 CacheReadInputTokens int `json:"cache_read_input_tokens"`
645 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
646 } `json:"usage"`
647 }
648 _ = json.Unmarshal([]byte(ev.Data), &m)
649 usage.InputTokens += m.Usage.InputTokens
650 usage.OutputTokens += m.Usage.OutputTokens
651 usage.CacheReadTokens += m.Usage.CacheReadInputTokens
652 usage.CacheWriteTokens += m.Usage.CacheCreationInputTokens
653 switch m.Delta.StopReason {
654 case "end_turn", "stop_sequence":
655 stop = StopEnd
656 case "max_tokens":
657 stop = StopLength
658 case "tool_use":
659 stop = StopToolUse
660 }
661 case "message_stop":
662 sendDone()
663 return
664 case "error":
665 var e struct {
666 Error struct {
667 Type string `json:"type"`
668 Message string `json:"message"`
669 } `json:"error"`
670 }
671 _ = json.Unmarshal([]byte(ev.Data), &e)
672 stop = StopError
673 finalErr = fmt.Errorf("anthropic %s: %s", e.Error.Type, e.Error.Message)
674 sendDone()
675 return
676 }
677 _ = activeIdx
678 }
679 }
680}