From 66d64545351232023081f62a055271794fa1bf12 Mon Sep 17 00:00:00 2001 From: keh4l <2461454684@qq.com> Date: Fri, 24 Apr 2026 20:38:22 +0800 Subject: [PATCH] feat(claude): add ttl to cache_control with default 5m Real Claude CLI traffic sends cache_control as `{"type":"ephemeral","ttl":"1h"}`. Our previous payload only sent `{"type":"ephemeral"}`, which is a bytewise mismatch with the official CLI and one more third-party detection signal. Policy: client-provided ttl is always passed through unchanged. Proxy-generated cache_control blocks default to 5m (vs Parrot's 1h) to avoid burning the 1h cache budget on automatic breakpoints while still aligning with the `ttl` field being present. - claude/constants.go: DefaultCacheControlTTL = "5m" - apicompat/types.go: new AnthropicCacheControl type with TTL field; AnthropicTool gains optional CacheControl pointer so the mimicry path can attach a cache breakpoint to tools[-1] later. - service/gateway_service.go: anthropicCacheControlPayload gains TTL; marshalAnthropicSystemTextBlock and rewriteSystemForNonClaudeCode emit ttl=5m by default. --- backend/internal/pkg/apicompat/types.go | 16 ++++++++++++---- backend/internal/pkg/claude/constants.go | 5 +++++ backend/internal/service/gateway_service.go | 8 ++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index cfc4b0aa..f8c6b75f 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -82,10 +82,18 @@ type AnthropicImageSource struct { // AnthropicTool describes a tool available to the model. type AnthropicTool struct { - Type string `json:"type,omitempty"` // e.g. "web_search_20250305" for server tools - Name string `json:"name"` - Description string `json:"description,omitempty"` - InputSchema json.RawMessage `json:"input_schema"` // JSON Schema object + Type string `json:"type,omitempty"` // e.g. "web_search_20250305" for server tools + Name string `json:"name"` + Description string `json:"description,omitempty"` + InputSchema json.RawMessage `json:"input_schema"` // JSON Schema object + CacheControl *AnthropicCacheControl `json:"cache_control,omitempty"` +} + +// AnthropicCacheControl 对应 Anthropic API 的 cache_control 字段。 +// ttl 默认由调用方决定;本项目策略见 claude.DefaultCacheControlTTL。 +type AnthropicCacheControl struct { + Type string `json:"type"` // "ephemeral" + TTL string `json:"ttl,omitempty"` // "5m" / "1h" / 省略=默认 5m(由 Anthropic 判定) } // AnthropicResponse is the non-streaming response from POST /v1/messages. diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 3c92c3e3..594d91ad 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -57,6 +57,11 @@ const APIKeyBetaHeader = BetaClaudeCode + "," + BetaInterleavedThinking + "," + // APIKeyHaikuBetaHeader Haiku 模型在 API-key 账号下使用的 anthropic-beta header(不包含 oauth / claude-code) const APIKeyHaikuBetaHeader = BetaInterleavedThinking +// DefaultCacheControlTTL 是网关代理为自己生成的 cache_control 块默认使用的 ttl。 +// 真实 Claude Code CLI 当前使用 "1h",但本仓策略是"客户端透传 ttl 优先; +// 客户端缺省时统一使用 5m",这样既不浪费 1h 缓存额度,也保留客户端自定义能力。 +const DefaultCacheControlTTL = "5m" + // FullClaudeCodeMimicryBetas 返回最"像"真实 Claude Code CLI 的完整 beta 列表, // 用于 OAuth 账号伪装成 Claude Code 时使用。 // 顺序与真实 CLI 抓包一致。 diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 4315808c..5ca5de0c 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -850,6 +850,7 @@ func (s *GatewayService) hashContent(content string) string { type anthropicCacheControlPayload struct { Type string `json:"type"` + TTL string `json:"ttl,omitempty"` } type anthropicSystemTextBlockPayload struct { @@ -898,7 +899,10 @@ func marshalAnthropicSystemTextBlock(text string, includeCacheControl bool) ([]b Text: text, } if includeCacheControl { - block.CacheControl = &anthropicCacheControlPayload{Type: "ephemeral"} + block.CacheControl = &anthropicCacheControlPayload{ + Type: "ephemeral", + TTL: claude.DefaultCacheControlTTL, + } } return json.Marshal(block) } @@ -3856,7 +3860,7 @@ func rewriteSystemForNonClaudeCode(body []byte, system any) []byte { { "type": "text", "text": claudeCodeSystemPrompt, - "cache_control": map[string]string{"type": "ephemeral"}, + "cache_control": map[string]string{"type": "ephemeral", "ttl": claude.DefaultCacheControlTTL}, }, } out, ok := setJSONValueBytes(body, "system", claudeCodeSystemBlock)