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