fix(anthropic): drop claude-code identity cache marker
oauth requests now exceed anthropic's 4-breakpoint cache_control limit when the conversation has 2+ user messages. previous layout emitted 5 markers: identity + system + tools + 2 user messages. drop the marker on the small claude-code identity line. it's a few tokens and gets folded into the cached prefix implicitly when the request matches turn-over-turn anyway. budget now: system + tools + last 2 user messages = 4. fits. reproduces the user-reported error: anthropic: http 400 ... A maximum of 4 blocks with cache_control may be provided. Found 5. verified by sending two consecutive prompts through zot rpc on an oauth credential -- first turn returns the assistant message cleanly, second turn does too instead of 400ing.
- commit
- 3ff6d9e6b7dadfd44563e7e0de9d5f9e74a8401b
- author
- patriceckhart · · 2026-04-19 10:39 UTC
- parent
- ebc5dad
- signature
- unsigned
- refs
- tag: v0.0.15
1 file · +8 -3
M internal/provider/anthropic.go +8 -3
| @@ -184,11 +184,16 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 184 | 184 | // System prompt assembly differs between api-key and OAuth modes. | |
| 185 | 185 | // OAuth requests MUST begin with the Claude Code identity line or | |
| 186 | 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. | |
| 187 | 193 | if c.oauthTok != "" { | |
| 188 | 194 | out.System = []anthSystemBlock{{ | |
| 189 | - | Type: "text", | |
| 190 | - | Text: claudeCodeIdentity, | |
| 191 | - | CacheControl: &anthCacheCtrl{Type: "ephemeral"}, | |
| 195 | + | Type: "text", | |
| 196 | + | Text: claudeCodeIdentity, | |
| 192 | 197 | }} | |
| 193 | 198 | if req.System != "" { | |
| 194 | 199 | out.System = append(out.System, anthSystemBlock{ | |
| @@ -184,11 +184,16 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 184 | // System prompt assembly differs between api-key and OAuth modes. | 184 | // System prompt assembly differs between api-key and OAuth modes. | ||
| 185 | // OAuth requests MUST begin with the Claude Code identity line or | 185 | // OAuth requests MUST begin with the Claude Code identity line or | ||
| 186 | // Anthropic rejects them (429 rate_limit_error with zero tokens used). | 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. | |||
| 187 | if c.oauthTok != "" { | 193 | if c.oauthTok != "" { | ||
| 188 | out.System = []anthSystemBlock{{ | 194 | out.System = []anthSystemBlock{{ | ||
| 189 | - | Type: "text", | 195 | + | Type: "text", |
| 190 | - | Text: claudeCodeIdentity, | 196 | + | Text: claudeCodeIdentity, |
| 191 | - | CacheControl: &anthCacheCtrl{Type: "ephemeral"}, | |||
| 192 | }} | 197 | }} | ||
| 193 | if req.System != "" { | 198 | if req.System != "" { | ||
| 194 | out.System = append(out.System, anthSystemBlock{ | 199 | out.System = append(out.System, anthSystemBlock{ | ||