v0.1.96 6684724

anthropic.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close