Skip to content

Commit d16e8f7

Browse files
author
SqlRush
committed
Latch cache beta headers from requests
1 parent fa91dd9 commit d16e8f7

5 files changed

Lines changed: 104 additions & 5 deletions

File tree

docs/claude-code-go-rewrite-plan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ test/parity/ # golden tests against TS/official behavior
132132
- 不启用工具时的 prompt -> streaming -> assistant 输出可跑通。
133133
- API request golden 能比对 headers/body 中关键字段。
134134

135+
- 本轮补充:Anthropic client 构造请求时会把显式 beta headers 与请求内容推导出的 beta headers 合并去重;当 messages/system content 或 tool definitions 中出现 `cache_control`/`cache_reference`,以及 messages 中出现 `cache_edits` payload 时,会自动带上 prompt-caching scope 与 cache-editing beta,避免 prompt-cache/cache-editing 请求依赖外部环境变量手动配置。
136+
135137
### M4: Tool framework、permissions、sandbox
136138

137139
产出:

docs/first-second-parity-audit.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Anthropic API 和 conversation:
9393
- Model registry now uses current Claude model IDs and aliases from the source snapshot, including Sonnet 4.6, Opus 4.6, Haiku 4.5, canonical-name rendering, and `[1m]` context variants.
9494
- OAuth support now includes production OAuth config, scope parsing, Claude.ai scope detection, auth URL construction, PKCE verifier/challenge, state generation, and expiry checks.
9595
- Session/history support now includes CC-compatible prompt history references, pasted text/image placeholder parsing, paste-cache hashing and retrieval, `history.jsonl` append/load, current-session-first up-arrow ordering, ctrl+r-style deduped timestamped history, `CLAUDE_CODE_SKIP_PROMPT_HISTORY`, remote session event pagination helpers, lenient transcript loading, legacy progress parent-bridge recovery, compact-boundary pruning, snip removal/relink replay, metadata entry collection, leaf UUID calculation, conversation-chain reconstruction, orphaned parallel tool-result recovery, content-replacement record loading/reconstruction, tombstone metadata delete/relink replay, and tombstone-style transcript message removal with a size guard.
96-
- Anthropic API layer now covers streaming accumulation, usage update/accumulation semantics, non-streaming max token cap, thinking-budget adjustment, retry/backoff with `Retry-After` and `x-should-retry`, context-overflow `max_tokens` retry adjustment, beta-header dedupe, custom request headers, basic prompt cache breakpoint/cache-reference/cache-edits placement, prompt dump JSONL capture for init/new user messages/non-streaming responses/stream chunks, and CC-compatible USD cost calculation for known Claude models including cache read/write, web search requests, and Opus 4.6 fast-tier pricing.
96+
- Anthropic API layer now covers streaming accumulation, usage update/accumulation semantics, non-streaming max token cap, thinking-budget adjustment, retry/backoff with `Retry-After` and `x-should-retry`, context-overflow `max_tokens` retry adjustment, beta-header dedupe, request-content beta latching for prompt-cache/cache-editing payloads, custom request headers, basic prompt cache breakpoint/cache-reference/cache-edits placement, prompt dump JSONL capture for init/new user messages/non-streaming responses/stream chunks, and CC-compatible USD cost calculation for known Claude models including cache read/write, web search requests, and Opus 4.6 fast-tier pricing.
9797
- Tool runtime now includes concurrency partitioning, ordered concurrent execution, interrupt behavior/defaults, max result size metadata, oversized result persistence, pre/post/permission-denied/permission-request hook dispatch, settings and local-plugin command-backed/HTTP hook execution for synchronous tool and conversation lifecycle hooks, hook-driven input updates/blocking/permission-request allow-deny, prompt/compact context injection, executor hook phase progress events, lifecycle progress events, retry/fallback model breadcrumbs, final-result/structured-error model attempt lists, structured API error status/type/request-id fields, headless stream-json progress events, snake_case token warning NDJSON payloads, lightweight compact NDJSON metadata, and pre-call cancellation checks.
9898
- Conversation runner can now use streaming clients, aggregate stream events into assistant messages, run tool calls through the orchestrator, preserve transcript append behavior, and apply CC-style per-message aggregate tool-result budget replacement before API requests with persisted replacement records for resume.
9999

@@ -106,7 +106,7 @@ The following items remain incomplete and must not be treated as done:
106106
- Interactive permission prompt flow: REPL dialogs, bridge/channel/swarm permission relays, user feedback images, prompt race handling, cancellation.
107107
- Full filesystem permission parity gaps that remain: skill-scope allow suggestions, complete local/local-jsx slash-command execution wiring, command permission UI/SDK surfacing, forked/remote/MCP/plugin/bundled SkillTool behavior, skill prompt shell injection, complete auto-memory override policy, and deeper platform-specific Windows/WSL bypass handling.
108108
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, mid-call cancellation for concrete tools, background task behavior, telemetry, schema-not-sent hints, and concrete tool-specific semantics.
109-
- Complete Anthropic API parity gaps that remain: full dynamic beta-header latching, ant-only dump gating and `/issue` integration, cost tracker/session restore integration, streaming-to-non-streaming fallback, gateway/proxy-specific headers, first-party/Bedrock/Vertex/Foundry client setup, OAuth refresh in retry loop, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
109+
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching beyond prompt-cache/cache-editing payloads, ant-only dump gating and `/issue` integration, cost tracker/session restore integration, streaming-to-non-streaming fallback, gateway/proxy-specific headers, first-party/Bedrock/Vertex/Foundry client setup, OAuth refresh in retry loop, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
110110
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.
111111
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, MDM/HKCU settings, managed-settings drop-in loading, settings cache/change detector, schema generation, plugin-only customization enforcement, all strict marketplace validations, and live reload/app-state sync.
112112
- Full session/history parity gaps that remain: large-file optimized transcript loading, preserved-segment edge cases beyond current relink/prune support, content-replacement feature-flag/runtime override and inherited subagent gap-fill details, complete async prompt-history lifecycle parity beyond current lock/buffer/undo paths, full pasted-image processing/runtime integration beyond current prompt image-cache/image-block/metadata path, remaining remote-history edge cases, sidechain/subagent transcript layout, and all session metadata entry types.

internal/api/anthropic/betas.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package anthropic
22

3-
import "strings"
3+
import (
4+
"strings"
5+
6+
"ccgo/internal/contracts"
7+
)
48

59
const (
610
PromptCachingScopeBetaHeader = "prompt-caching-scope-2024-07-31"
@@ -32,3 +36,58 @@ func MergeBetaHeaders(groups ...[]string) []string {
3236
func BetaHeaderValue(headers []string) string {
3337
return strings.Join(MergeBetaHeaders(headers), ",")
3438
}
39+
40+
func DynamicBetaHeaders(payload any) []string {
41+
request, ok := payload.(Request)
42+
if !ok {
43+
return nil
44+
}
45+
var betas []string
46+
if requestUsesPromptCaching(request) {
47+
betas = append(betas, PromptCachingScopeBetaHeader)
48+
}
49+
if requestUsesCacheEditing(request) {
50+
betas = append(betas, CacheEditingBetaHeader)
51+
}
52+
return betas
53+
}
54+
55+
func requestUsesPromptCaching(request Request) bool {
56+
for _, msg := range request.Messages {
57+
if contentBlocksUsePromptCaching(msg.Content) {
58+
return true
59+
}
60+
}
61+
if systemBlocks, ok := request.System.([]contracts.ContentBlock); ok && contentBlocksUsePromptCaching(systemBlocks) {
62+
return true
63+
}
64+
for _, tool := range request.Tools {
65+
if tool.CacheControl != nil {
66+
return true
67+
}
68+
}
69+
return false
70+
}
71+
72+
func requestUsesCacheEditing(request Request) bool {
73+
for _, msg := range request.Messages {
74+
for _, block := range msg.Content {
75+
if block.Type == contracts.ContentCacheEdits || len(block.Edits) > 0 {
76+
return true
77+
}
78+
}
79+
}
80+
return false
81+
}
82+
83+
func contentBlocksUsePromptCaching(blocks []contracts.ContentBlock) bool {
84+
for _, block := range blocks {
85+
if block.CacheControl != nil || strings.TrimSpace(block.CacheReference) != "" {
86+
return true
87+
}
88+
if block.Type == contracts.ContentCacheEdits || len(block.Edits) > 0 {
89+
return true
90+
}
91+
}
92+
return false
93+
}

internal/api/anthropic/client.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,9 @@ func (c *Client) newJSONRequestWithDump(ctx context.Context, path string, payloa
289289
req.Header.Set("content-type", "application/json")
290290
req.Header.Set("accept", "application/json")
291291
req.Header.Set("anthropic-version", c.version())
292-
if len(c.Beta) > 0 {
293-
req.Header.Set("anthropic-beta", BetaHeaderValue(c.Beta))
292+
betas := MergeBetaHeaders(c.Beta, DynamicBetaHeaders(payload))
293+
if len(betas) > 0 {
294+
req.Header.Set("anthropic-beta", BetaHeaderValue(betas))
294295
}
295296
for key, values := range c.Headers {
296297
for _, value := range values {

internal/api/anthropic/client_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,43 @@ func TestBetaHeadersDedupedAndCustomHeadersApplied(t *testing.T) {
332332
}
333333
}
334334

335+
func TestCreateMessageAddsDynamicCacheBetaHeaders(t *testing.T) {
336+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
337+
if got := r.Header.Get("anthropic-beta"); got != "one,prompt-caching-scope-2024-07-31,cache-editing-2025-01-24" {
338+
t.Fatalf("anthropic-beta = %q", got)
339+
}
340+
w.Header().Set("content-type", "application/json")
341+
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","role":"assistant","model":"sonnet","content":[]}`))
342+
}))
343+
defer server.Close()
344+
345+
client := NewClient(WithBaseURL(server.URL), WithBeta("one", PromptCachingScopeBetaHeader))
346+
_, err := client.CreateMessage(context.Background(), Request{
347+
Model: "sonnet",
348+
MaxTokens: 32,
349+
Messages: []contracts.APIMessage{{
350+
Role: "user",
351+
Content: []contracts.ContentBlock{
352+
{
353+
Type: contracts.ContentText,
354+
Text: "hello",
355+
CacheControl: &contracts.CacheControl{Type: "ephemeral", Scope: "global"},
356+
},
357+
{
358+
Type: contracts.ContentCacheEdits,
359+
Edits: []contracts.CacheEdit{{
360+
Type: "delete",
361+
CacheReference: "toolu_old",
362+
}},
363+
},
364+
},
365+
}},
366+
})
367+
if err != nil {
368+
t.Fatal(err)
369+
}
370+
}
371+
335372
func TestAddCacheBreakpoints(t *testing.T) {
336373
messages := []contracts.APIMessage{
337374
{Role: "user", Content: []contracts.ContentBlock{{Type: contracts.ContentToolResult, ToolUseID: "toolu_1"}}},

0 commit comments

Comments
 (0)