1package provider
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "strings"
13 "sync"
14 "time"
15)
16
17const anthropicDefaultBaseURL = "https://api.anthropic.com"
18const anthropicAPIVersion = "2023-06-01"
19
20// anthropicMessagesURL builds the Messages endpoint for an API root. A base
21// URL that already ends in a numeric version (for example /v1) gets
22// /messages appended directly; a bare or provider-specific root gets the
23// conventional /v1/messages suffix.
24func anthropicMessagesURL(baseURL string) string {
25 baseURL = strings.TrimRight(baseURL, "/")
26 if versionSegmentSuffix.MatchString(baseURL) {
27 return baseURL + "/messages"
28 }
29 return baseURL + "/v1/messages"
30}
31
32// Stealth identity used when talking to Anthropic via subscription OAuth.
33// These values mimic the official Claude Code CLI so Anthropic's edge
34// accepts the request; diverging from them causes 429 rate_limit_error
35// or 403 on the very first request.
36const (
37 claudeCodeVersion = "2.1.280"
38 claudeCodeIdentity = "You are Claude Code, Anthropic's official CLI for Claude."
39)
40
41// Claude Code's canonical tool casing. When running under OAuth we must
42// advertise tool names that match this list (case-insensitive lookup),
43// because Anthropic's backend cross-checks them.
44var claudeCodeToolNames = map[string]string{
45 "read": "Read",
46 "write": "Write",
47 "edit": "Edit",
48 "bash": "Bash",
49 "grep": "Grep",
50 "glob": "Glob",
51}
52
53func toClaudeCodeToolName(name string) string {
54 if cc, ok := claudeCodeToolNames[strings.ToLower(name)]; ok {
55 return cc
56 }
57 return name
58}
59
60func fromClaudeCodeToolName(name string, tools []Tool) string {
61 lower := strings.ToLower(name)
62 for _, t := range tools {
63 if strings.ToLower(t.Name) == lower {
64 return t.Name
65 }
66 }
67 return name
68}
69
70// anthropicClient implements Client against the Anthropic Messages API.
71type anthropicClient struct {
72 apiKey string
73 baseURL string
74 oauthTok string // when non-empty, send Bearer auth instead of x-api-key
75 http *http.Client
76
77 // name overrides the default "anthropic" identity. Anthropic-Messages-
78 // compatible third-party endpoints (kimi-coding, fireworks, minimax,
79 // vercel-ai-gateway, etc.) set this so cost lookup / logging / image-
80 // stripping logic can route on a stable provider id.
81 name string
82
83 // headers carries extra request headers (e.g. Kimi Code's X-Msh-*).
84 headers map[string]string
85
86 modelOverridesMu sync.RWMutex
87 modelOverrides map[string]Model
88}
89
90// NewAnthropic creates an Anthropic client using an API key. baseURL may be empty.
91func NewAnthropic(apiKey, baseURL string) Client {
92 if baseURL == "" {
93 baseURL = anthropicDefaultBaseURL
94 }
95 return &anthropicClient{
96 apiKey: apiKey,
97 baseURL: strings.TrimRight(baseURL, "/"),
98 http: &http.Client{Timeout: 0},
99 }
100}
101
102// NewAnthropicOAuth creates an Anthropic client using a subscription OAuth access token.
103func NewAnthropicOAuth(accessToken, baseURL string) Client {
104 if baseURL == "" {
105 baseURL = anthropicDefaultBaseURL
106 }
107 return &anthropicClient{
108 oauthTok: accessToken,
109 baseURL: strings.TrimRight(baseURL, "/"),
110 http: &http.Client{Timeout: 0},
111 }
112}
113
114func (c *anthropicClient) Name() string {
115 if c.name != "" {
116 return c.name
117 }
118 return "anthropic"
119}
120
121func (c *anthropicClient) SetModelMetadata(model Model) {
122 model = cloneModel(model)
123 c.modelOverridesMu.Lock()
124 if c.modelOverrides == nil {
125 c.modelOverrides = make(map[string]Model)
126 }
127 c.modelOverrides[model.ID] = model
128 c.modelOverridesMu.Unlock()
129}
130
131func (c *anthropicClient) modelForRequest(id string) (Model, error) {
132 id = strings.TrimSpace(id)
133 c.modelOverridesMu.RLock()
134 model, ok := c.modelOverrides[id]
135 if ok {
136 model = cloneModel(model)
137 }
138 c.modelOverridesMu.RUnlock()
139 if ok {
140 return model, nil
141 }
142 model, err := FindModel(c.Name(), id)
143 if err != nil {
144 model, err = FindModel("", id)
145 }
146 return model, err
147}
148
149// ---- wire types ----
150
151type anthTextBlock struct {
152 Type string `json:"type"` // "text"
153 Text string `json:"text"`
154 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
155}
156
157type anthImageSource struct {
158 Type string `json:"type"` // "base64"
159 MediaType string `json:"media_type"`
160 Data string `json:"data"`
161}
162
163type anthImageBlock struct {
164 Type string `json:"type"` // "image"
165 Source anthImageSource `json:"source"`
166 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
167}
168
169type anthToolUseBlock struct {
170 Type string `json:"type"` // "tool_use"
171 ID string `json:"id"`
172 Name string `json:"name"`
173 Input json.RawMessage `json:"input"`
174 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
175}
176
177type anthToolResultBlock struct {
178 Type string `json:"type"` // "tool_result"
179 ToolUseID string `json:"tool_use_id"`
180 Content json.RawMessage `json:"content"` // string or array of blocks
181 IsError bool `json:"is_error,omitempty"`
182 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
183}
184
185type anthCacheCtrl struct {
186 Type string `json:"type"` // "ephemeral"
187 TTL string `json:"ttl,omitempty"`
188}
189
190type anthMessage struct {
191 Role string `json:"role"`
192 Content []interface{} `json:"content"`
193}
194
195type anthSystemBlock struct {
196 Type string `json:"type"` // "text"
197 Text string `json:"text"`
198 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
199}
200
201type anthTool struct {
202 Name string `json:"name"`
203 Description string `json:"description,omitempty"`
204 InputSchema json.RawMessage `json:"input_schema"`
205 CacheControl *anthCacheCtrl `json:"cache_control,omitempty"`
206}
207
208type anthThinking struct {
209 // Type is "enabled" for budget-based thinking (Opus 4.6 and
210 // earlier) or "adaptive" for adaptive-thinking models (Opus 4.7+).
211 Type string `json:"type"`
212 // BudgetTokens is only sent for Type=="enabled". Adaptive models
213 // reject it (400), so it is omitted for them.
214 BudgetTokens int `json:"budget_tokens,omitempty"`
215}
216
217// anthOutputConfig carries the effort knob used by adaptive-thinking
218// models to control reasoning depth ("low"|"medium"|"high"|"xhigh").
219type anthOutputConfig struct {
220 Effort string `json:"effort,omitempty"`
221}
222
223type anthRequest struct {
224 Model string `json:"model"`
225 MaxTokens int `json:"max_tokens"`
226 System []anthSystemBlock `json:"system,omitempty"`
227 Messages []anthMessage `json:"messages"`
228 Tools []anthTool `json:"tools,omitempty"`
229 Temperature *float32 `json:"temperature,omitempty"`
230 Thinking *anthThinking `json:"thinking,omitempty"`
231 OutputConfig *anthOutputConfig `json:"output_config,omitempty"`
232 Stream bool `json:"stream"`
233}
234
235// usesAdaptiveThinking reports whether a model only supports the
236// adaptive thinking mode (Opus 4.7 and later, including Opus 5). These models reject
237// explicit thinking budgets and non-default sampling parameters. The
238// catalog flag is authoritative; the id-substring fallback catches
239// the same family when reached through an Anthropic-Messages-
240// compatible proxy whose catalog row predates the flag.
241func usesAdaptiveThinking(m Model) bool {
242 if m.AdaptiveThinking || m.AdaptiveThinkingCompat {
243 return true
244 }
245 id := strings.ToLower(m.ID)
246 for _, marker := range []string{"opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8", "opus-5", "opus.5", "sonnet-5", "fable-5"} {
247 if strings.Contains(id, marker) {
248 return true
249 }
250 }
251 return false
252}
253
254// ---- request building ----
255
256func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) {
257 req.Model = strings.TrimSpace(req.Model)
258 m, err := c.modelForRequest(req.Model)
259 if err != nil {
260 return nil, err
261 }
262 maxTok := req.MaxTokens
263 if maxTok <= 0 {
264 maxTok = m.MaxOutput
265 }
266
267 reasoning := ClampReasoningForModel(m, req.Reasoning)
268 adaptive := usesAdaptiveThinking(m)
269 adaptiveEffort := adaptive && !m.AdaptiveThinkingCompat
270
271 out := &anthRequest{
272 Model: req.Model,
273 MaxTokens: maxTok,
274 Stream: true,
275 }
276 // Anthropic's adaptive-thinking models reject non-default sampling
277 // params. Compatible implementations may use the adaptive thinking type
278 // while continuing to support temperature.
279 if !adaptiveEffort {
280 out.Temperature = req.Temperature
281 }
282
283 stableSystem, dynamicSystem, messages := systemWithDeveloperContext(req)
284
285 // System prompt assembly differs between api-key and OAuth modes.
286 // OAuth requests MUST begin with the Claude Code identity line or
287 // Anthropic rejects them (429 rate_limit_error with zero tokens used).
288 //
289 // Cache budget: anthropic caps cache_control to 4 breakpoints per
290 // request. We spend them on:
291 // 1. claude-code identity (OAuth only; stable forever)
292 // 2. user system prompt (changes per-session at most)
293 // 3. last tool definition (tools change rarely)
294 // 4. last message block (advances every turn)
295 //
296 // The identity line gets its OWN cache_control so the prefix
297 // [identity] is cacheable independently of the user system
298 // prompt. Without that, the cache prefix starts after block 2
299 // and any drift in the user prompt (e.g. the Current date
300 // line flipping at midnight) invalidates everything, including
301 // the 17 identity tokens we have to re-send every request
302 // forever.
303 if c.oauthTok != "" {
304 out.System = []anthSystemBlock{{
305 Type: "text",
306 Text: claudeCodeIdentity,
307 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
308 }}
309 if stableSystem != "" {
310 out.System = append(out.System, anthSystemBlock{
311 Type: "text",
312 Text: stableSystem,
313 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
314 })
315 }
316 } else if stableSystem != "" {
317 out.System = []anthSystemBlock{{
318 Type: "text",
319 Text: stableSystem,
320 CacheControl: &anthCacheCtrl{Type: "ephemeral"},
321 }}
322 }
323 if dynamicSystem != "" {
324 out.System = append(out.System, anthSystemBlock{Type: "text", Text: dynamicSystem})
325 }
326
327 if reasoning != "" && m.Reasoning {
328 if adaptive {
329 // Adaptive-thinking models decide when and how much to think and
330 // reject explicit budgets. Anthropic models also accept the
331 // output_config.effort extension; compatible implementations may not.
332 out.Thinking = &anthThinking{Type: "adaptive"}
333 if adaptiveEffort {
334 if effort := AnthropicAdaptiveEffort(reasoning); effort != "" {
335 out.OutputConfig = &anthOutputConfig{Effort: effort}
336 }
337 }
338 } else {
339 budget := anthropicReasoningBudget(reasoning)
340 if budget > 0 {
341 // Reasoning requires max_tokens > budget. Keep at least a small
342 // answer budget while respecting the model's advertised output cap.
343 const minAnswerTokens = 1024
344 if m.MaxOutput > minAnswerTokens && budget >= m.MaxOutput {
345 budget = m.MaxOutput - minAnswerTokens
346 }
347 out.Thinking = &anthThinking{Type: "enabled", BudgetTokens: budget}
348 if out.MaxTokens <= budget {
349 out.MaxTokens = budget + minAnswerTokens
350 if m.MaxOutput > 0 && out.MaxTokens > m.MaxOutput {
351 out.MaxTokens = m.MaxOutput
352 }
353 }
354 }
355 }
356 }
357
358 for _, t := range activeToolDefinitions(req.Tools, messages) {
359 name := t.Name
360 if c.oauthTok != "" {
361 name = toClaudeCodeToolName(name)
362 }
363 out.Tools = append(out.Tools, anthTool{
364 Name: name,
365 Description: t.Description,
366 InputSchema: t.Schema,
367 })
368 }
369 // Cache the last tool definition (applies cache breakpoint to the whole tools array).
370 if n := len(out.Tools); n > 0 {
371 out.Tools[n-1].CacheControl = &anthCacheCtrl{Type: "ephemeral"}
372 }
373
374 // Convert messages. Anthropic's wire format has only "user" and
375 // "assistant" roles; tool_result blocks live inside user messages.
376 // Some image-capable providers (for example Gemini image generation)
377 // can emit assistant image blocks. Anthropic only accepts image blocks
378 // in user messages, so strip assistant-side images when switching
379 // providers mid-session. The saved-path TextBlock emitted beside the
380 // image keeps the artifact reachable in chat.
381 //
382 // CRITICAL: tool_result blocks go into their OWN new user
383 // message, they are NOT merged into the preceding user message.
384 // Merging would mutate the prior user message's content array
385 // between turn N and turn N+1: turn N caches the prefix ending at
386 // [user: "read sample.ts"], turn N+1 sends
387 // [user: "read sample.ts" + tool_result=...] which is a
388 // different block sequence, busting the cache prefix match.
389 // Anthropic's API happily accepts consecutive user messages, and
390 // emitting them separately keeps each message bit-stable across
391 // turns, so the cache prefix matches for the entire history up
392 // to the newest block.
393 messages = RepairOrphanedToolResults(messages)
394 for _, msg := range messages {
395 renameTools := c.oauthTok != ""
396 switch msg.Role {
397 case RoleUser:
398 out.Messages = append(out.Messages, anthMessage{
399 Role: "user",
400 Content: convertAnthContent(msg.Content, renameTools),
401 })
402 case RoleTool:
403 out.Messages = append(out.Messages, anthMessage{
404 Role: "user",
405 Content: convertAnthContent(msg.Content, renameTools),
406 })
407 case RoleAssistant:
408 out.Messages = append(out.Messages, anthMessage{
409 Role: "assistant",
410 Content: convertAnthContent(filterAnthAssistantContent(msg.Content), renameTools),
411 })
412 }
413 }
414
415 // Tag the LAST user message with cache_control. Spends the 4th
416 // breakpoint. For prefixes under ~1024 tokens (Anthropic's
417 // minimum cacheable block size for Opus), no cache is written.
418 tagLastUserCache(out.Messages)
419
420 return out, nil
421}
422
423// tagLastUserCache marks the last block of the most recent user
424// message. One marker; combined with identity + systemPrompt +
425// last-tool, spends Anthropic's 4-breakpoint budget.
426func tagLastUserCache(msgs []anthMessage) {
427 for i := len(msgs) - 1; i >= 0; i-- {
428 if msgs[i].Role == "user" {
429 markLastBlockEphemeral(msgs[i].Content)
430 return
431 }
432 }
433}
434
435// markLastBlockEphemeral sets CacheControl on the last entry in blocks
436// regardless of whether it's a text, image, tool_use, or tool_result.
437// Each block type carries its own CacheControl pointer so we type-
438// switch + reassign the slice element.
439func markLastBlockEphemeral(blocks []interface{}) {
440 if len(blocks) == 0 {
441 return
442 }
443 i := len(blocks) - 1
444 cc := &anthCacheCtrl{Type: "ephemeral"}
445 switch v := blocks[i].(type) {
446 case anthTextBlock:
447 v.CacheControl = cc
448 blocks[i] = v
449 case anthImageBlock:
450 v.CacheControl = cc
451 blocks[i] = v
452 case anthToolUseBlock:
453 v.CacheControl = cc
454 blocks[i] = v
455 case anthToolResultBlock:
456 v.CacheControl = cc
457 blocks[i] = v
458 }
459}
460
461func anthropicReasoningBudget(level string) int {
462 return ReasoningBudget(level)
463}
464
465func filterAnthAssistantContent(blocks []Content) []Content {
466 filtered := make([]Content, 0, len(blocks))
467 for _, b := range blocks {
468 if _, ok := b.(ImageBlock); ok {
469 continue
470 }
471 filtered = append(filtered, b)
472 }
473 return filtered
474}
475
476func convertAnthContent(blocks []Content, renameTools bool) []interface{} {
477 out := make([]interface{}, 0, len(blocks))
478 for _, b := range blocks {
479 switch v := b.(type) {
480 case TextBlock:
481 if v.Text == "" {
482 continue
483 }
484 out = append(out, anthTextBlock{Type: "text", Text: v.Text})
485 case ImageBlock:
486 data, mime := anthShrinkImageBytesIfTooBig(v.Data, v.MimeType)
487 out = append(out, anthImageBlock{
488 Type: "image",
489 Source: anthImageSource{
490 Type: "base64",
491 MediaType: mime,
492 Data: base64.StdEncoding.EncodeToString(data),
493 },
494 })
495 case ToolCallBlock:
496 args := v.Arguments
497 if len(args) == 0 || !json.Valid(args) {
498 args = json.RawMessage("{}")
499 }
500 name := v.Name
501 if renameTools {
502 name = toClaudeCodeToolName(name)
503 }
504 out = append(out, anthToolUseBlock{
505 Type: "tool_use", ID: v.ID, Name: name, Input: args,
506 })
507 case ToolResultBlock:
508 // Flatten content to a string if all text; else array of blocks.
509 content, _ := anthBuildToolResultContent(v.Content)
510 out = append(out, anthToolResultBlock{
511 Type: "tool_result",
512 ToolUseID: v.CallID,
513 Content: content,
514 IsError: v.IsError,
515 })
516 }
517 }
518 return out
519}
520
521func anthBuildToolResultContent(blocks []Content) (json.RawMessage, error) {
522 onlyText := true
523 var sb strings.Builder
524 for _, b := range blocks {
525 if tb, ok := b.(TextBlock); ok {
526 if sb.Len() > 0 {
527 sb.WriteString("\n")
528 }
529 sb.WriteString(tb.Text)
530 } else {
531 onlyText = false
532 break
533 }
534 }
535 if onlyText {
536 if sb.Len() == 0 {
537 return json.Marshal("")
538 }
539 return json.Marshal(sb.String())
540 }
541 // Array form: text + image blocks.
542 arr := make([]interface{}, 0, len(blocks))
543 for _, b := range blocks {
544 switch v := b.(type) {
545 case TextBlock:
546 arr = append(arr, anthTextBlock{Type: "text", Text: v.Text})
547 case ImageBlock:
548 data, mime := anthShrinkImageBytesIfTooBig(v.Data, v.MimeType)
549 arr = append(arr, anthImageBlock{
550 Type: "image",
551 Source: anthImageSource{
552 Type: "base64",
553 MediaType: mime,
554 Data: base64.StdEncoding.EncodeToString(data),
555 },
556 })
557 }
558 }
559 return json.Marshal(arr)
560}
561
562// ---- streaming ----
563
564func (c *anthropicClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
565 if err := ValidateFastMode(c.Name(), req.FastMode); err != nil {
566 return nil, err
567 }
568 wire, err := c.buildRequest(req)
569 if err != nil {
570 return nil, err
571 }
572 body, err := json.Marshal(wire)
573 if err != nil {
574 return nil, err
575 }
576
577 // Optional debug dump: when $ZUT_DEBUG_ANTHROPIC is a file path
578 // we append every outgoing request body to it, one JSON object
579 // per line. Useful for diffing turn N vs turn N+1 to understand
580 // why the cache prefix isn't matching.
581 if dump := os.Getenv("ZUT_DEBUG_ANTHROPIC"); dump != "" {
582 if f, derr := os.OpenFile(dump, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); derr == nil {
583 _, _ = f.Write(body)
584 _, _ = f.Write([]byte{'\n'})
585 _ = f.Close()
586 }
587 }
588
589 endpoint := anthropicMessagesURL(c.baseURL)
590 newReq := func() (*http.Request, error) {
591 httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
592 if err != nil {
593 return nil, err
594 }
595 httpReq.Header.Set("content-type", "application/json")
596 httpReq.Header.Set("anthropic-version", anthropicAPIVersion)
597 if c.oauthTok != "" {
598 // Claude-Code-shaped request: identical headers and values as the
599 // official CLI. Any drift triggers Anthropic's anti-abuse check and
600 // rate-limits (or outright blocks) the request.
601 httpReq.Header.Set("accept", "application/json")
602 httpReq.Header.Set("authorization", "Bearer "+c.oauthTok)
603 httpReq.Header.Set("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14")
604 httpReq.Header.Set("anthropic-dangerous-direct-browser-access", "true")
605 httpReq.Header.Set("user-agent", "claude-cli/"+claudeCodeVersion)
606 httpReq.Header.Set("x-app", "cli")
607 // Remove x-api-key entirely by NOT setting it.
608 } else {
609 httpReq.Header.Set("accept", "text/event-stream")
610 httpReq.Header.Set("x-api-key", c.apiKey)
611 }
612 // Extra headers (set by anthropic-messages-compatible third parties
613 // — kimi-coding's X-Msh-*, copilot's Editor-Plugin-Version, etc.).
614 // Applied last so callers can override defaults if they really need to.
615 for k, v := range c.headers {
616 httpReq.Header.Set(k, v)
617 }
618 setOpenCodeGoHeaders(httpReq.Header, c.Name(), req.Context)
619 return httpReq, nil
620 }
621
622 resp, err := doStreamWithRetry(ctx, c.http, newReq, req.Lifecycle)
623 if err != nil {
624 return nil, fmt.Errorf("anthropic: %w", err)
625 }
626 if resp.StatusCode != http.StatusOK {
627 b, _ := io.ReadAll(resp.Body)
628 resp.Body.Close()
629 return nil, fmt.Errorf("anthropic: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
630 }
631
632 out := make(chan Event, 16)
633 go c.runStream(ctx, resp, req, out)
634 return out, nil
635}
636
637func (c *anthropicClient) runStream(ctx context.Context, resp *http.Response, req Request, out chan<- Event) {
638 defer close(out)
639 defer resp.Body.Close()
640
641 // Use the same runtime-owned metadata as buildRequest so cost and
642 // capability handling stay isolated from other SDK runtimes.
643 model, _ := c.modelForRequest(req.Model)
644 out <- EventStart{Model: req.Model, Provider: c.Name()}
645
646 raw := make(chan sseEvent, 16)
647 go readSSE(resp.Body, raw)
648
649 // State for assembling the assistant message. Blocks are indexed
650 // by their `index` field from Anthropic so we can preserve the
651 // interleaved order the model emitted them in (text may come
652 // before OR after tool_use; mixing both happens frequently).
653 type blockEntry struct {
654 kind string // "text" | "tool_use"
655 textBuf strings.Builder
656 toolCall ToolCallBlock
657 toolArgs strings.Builder
658 }
659 var (
660 blocks = map[int]*blockEntry{}
661 blockOrder []int // insertion order of indexes
662 activeIdx = -1
663 usage Usage
664 stop = StopEnd
665 finalErr error
666 )
667 _ = activeIdx // read-only indicator used for legacy parity
668
669 registerBlock := func(idx int, kind string) *blockEntry {
670 if be, ok := blocks[idx]; ok {
671 return be
672 }
673 be := &blockEntry{kind: kind}
674 blocks[idx] = be
675 blockOrder = append(blockOrder, idx)
676 return be
677 }
678
679 assembleMsg := func() Message {
680 content := []Content{}
681 for _, idx := range blockOrder {
682 be := blocks[idx]
683 switch be.kind {
684 case "text":
685 if be.textBuf.Len() > 0 {
686 content = append(content, TextBlock{Text: be.textBuf.String()})
687 }
688 case "tool_use":
689 tc := be.toolCall
690 args := be.toolArgs.String()
691 if args == "" {
692 args = "{}"
693 }
694 tc.Arguments = json.RawMessage(args)
695 content = append(content, tc)
696 }
697 }
698 return Message{Role: RoleAssistant, Content: content, Time: time.Now()}
699 }
700
701 sendDone := func() {
702 usage.CostUSD = ComputeCost(model, usage)
703 out <- EventUsage{Usage: usage}
704 out <- EventDone{Stop: stop, Err: finalErr, Message: assembleMsg()}
705 }
706
707 for {
708 select {
709 case <-ctx.Done():
710 stop = StopAborted
711 finalErr = ctx.Err()
712 sendDone()
713 return
714 case ev, ok := <-raw:
715 if !ok {
716 sendDone()
717 return
718 }
719 if ev.Err != nil {
720 stop = StopError
721 finalErr = fmt.Errorf("read SSE: %w", ev.Err)
722 sendDone()
723 return
724 }
725 // Parse event payload based on event: type.
726 var payload map[string]json.RawMessage
727 if err := json.Unmarshal([]byte(ev.Data), &payload); err != nil {
728 continue
729 }
730 switch ev.Event {
731 case "content_block_start":
732 var idx int
733 if b, ok := payload["index"]; ok {
734 _ = json.Unmarshal(b, &idx)
735 }
736 var block struct {
737 Type string `json:"type"`
738 ID string `json:"id"`
739 Name string `json:"name"`
740 Text string `json:"text"`
741 Input json.RawMessage `json:"input"`
742 }
743 if b, ok := payload["content_block"]; ok {
744 _ = json.Unmarshal(b, &block)
745 }
746 activeIdx = idx
747 switch block.Type {
748 case "tool_use":
749 name := block.Name
750 if c.oauthTok != "" {
751 name = fromClaudeCodeToolName(name, req.Tools)
752 }
753 be := registerBlock(idx, "tool_use")
754 be.toolCall = ToolCallBlock{ID: block.ID, Name: name}
755 out <- EventToolStart{ID: block.ID, Name: name}
756 case "text":
757 registerBlock(idx, "text")
758 case "thinking":
759 // not surfaced
760 }
761 case "content_block_delta":
762 var idx int
763 if b, ok := payload["index"]; ok {
764 _ = json.Unmarshal(b, &idx)
765 }
766 var d struct {
767 Type string `json:"type"`
768 Text string `json:"text"`
769 PartialJSON string `json:"partial_json"`
770 Thinking string `json:"thinking"`
771 }
772 if b, ok := payload["delta"]; ok {
773 _ = json.Unmarshal(b, &d)
774 }
775 switch d.Type {
776 case "text_delta":
777 if be, ok := blocks[idx]; ok && be.kind == "text" {
778 be.textBuf.WriteString(d.Text)
779 }
780 out <- EventTextDelta{Delta: d.Text}
781 case "input_json_delta":
782 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
783 be.toolArgs.WriteString(d.PartialJSON)
784 out <- EventToolArgs{ID: be.toolCall.ID, Delta: d.PartialJSON}
785 }
786 case "thinking_delta":
787 // Not surfaced in v1.
788 }
789 case "content_block_stop":
790 var idx int
791 if b, ok := payload["index"]; ok {
792 _ = json.Unmarshal(b, &idx)
793 }
794 if be, ok := blocks[idx]; ok && be.kind == "tool_use" {
795 out <- EventToolEnd{ID: be.toolCall.ID}
796 }
797 activeIdx = -1
798 case "message_start":
799 var m struct {
800 Message struct {
801 Usage struct {
802 InputTokens int `json:"input_tokens"`
803 OutputTokens int `json:"output_tokens"`
804 CacheReadInputTokens int `json:"cache_read_input_tokens"`
805 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
806 } `json:"usage"`
807 } `json:"message"`
808 }
809 _ = json.Unmarshal([]byte(ev.Data), &m)
810 // Anthropic sends cumulative values on message_start and
811 // again on message_delta (refreshed), so assign, don't
812 // accumulate. Accumulating doubles cache_creation_input
813 // which can be 50-70% of cost.
814 usage.InputTokens = m.Message.Usage.InputTokens
815 usage.OutputTokens = m.Message.Usage.OutputTokens
816 usage.CacheReadTokens = m.Message.Usage.CacheReadInputTokens
817 usage.CacheWriteTokens = m.Message.Usage.CacheCreationInputTokens
818 case "message_delta":
819 var m struct {
820 Delta struct {
821 StopReason string `json:"stop_reason"`
822 } `json:"delta"`
823 Usage struct {
824 InputTokens int `json:"input_tokens"`
825 OutputTokens int `json:"output_tokens"`
826 CacheReadInputTokens int `json:"cache_read_input_tokens"`
827 CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
828 } `json:"usage"`
829 }
830 _ = json.Unmarshal([]byte(ev.Data), &m)
831 // Refresh usage from the latest cumulative totals
832 // Anthropic provides. Only apply non-zero fields in case
833 // a given delta only carries output tokens.
834 if m.Usage.InputTokens > 0 {
835 usage.InputTokens = m.Usage.InputTokens
836 }
837 if m.Usage.OutputTokens > 0 {
838 usage.OutputTokens = m.Usage.OutputTokens
839 }
840 if m.Usage.CacheReadInputTokens > 0 {
841 usage.CacheReadTokens = m.Usage.CacheReadInputTokens
842 }
843 if m.Usage.CacheCreationInputTokens > 0 {
844 usage.CacheWriteTokens = m.Usage.CacheCreationInputTokens
845 }
846 switch m.Delta.StopReason {
847 case "end_turn", "stop_sequence":
848 stop = StopEnd
849 case "max_tokens":
850 stop = StopLength
851 case "tool_use":
852 stop = StopToolUse
853 }
854 case "message_stop":
855 sendDone()
856 return
857 case "error":
858 var e struct {
859 Error struct {
860 Type string `json:"type"`
861 Message string `json:"message"`
862 } `json:"error"`
863 }
864 _ = json.Unmarshal([]byte(ev.Data), &e)
865 stop = StopError
866 finalErr = fmt.Errorf("anthropic %s: %s", e.Error.Type, e.Error.Message)
867 sendDone()
868 return
869 }
870 _ = activeIdx
871 }
872 }
873}