@@ -1910,24 +1910,45 @@ func preferContentKey(hasToken bool) bool {
19101910 return ! hasToken
19111911}
19121912
1913+ // stableInstrHeadSegs 是 instructions 稳定前缀的段数:"\n\n" 切段后只取前 N 段
1914+ // 参与 prompt_cache_key 哈希。Claude 侧 instructions = system 参数 +
1915+ // role=system 消息顺序 join,实测头 16 段跨轮逐段同哈希、尾部每轮追加约 18 段;
1916+ // 只哈希稳定头则同会话跨轮 key 不变,上游 prefix-cache 才能命中。N=16 覆盖实测
1917+ // 稳定区并留出余量;不同会话/不同 system 头天然不同段,不会误撞。
1918+ const stableInstrHeadSegs = 16
1919+
1920+ // stableInstructionsHead 返回参与缓存 key 的 instructions 稳定前缀:按 "\n\n"
1921+ // 切段取前 stableInstrHeadSegs 段 join。段数不足时返回原文(短 instructions 不
1922+ // 截断);尾部追加段被排除在 key 之外,但仍会原样发往上游,不丢上下文。
1923+ func stableInstructionsHead (instr string ) string {
1924+ segs := strings .Split (instr , "\n \n " )
1925+ if len (segs ) <= stableInstrHeadSegs {
1926+ return instr
1927+ }
1928+ return strings .Join (segs [:stableInstrHeadSegs ], "\n \n " )
1929+ }
1930+
19131931// contentPromptCacheKey 对上游请求体(已 marshal 的 chat/responses JSON)的
1914- // 公共前缀求 SHA-256 派生稳定 key:system 提示、工具清单、instructions 与
1915- // 首个 user message 决定缓存前缀;assistant/tool 后续消息会随轮次变化,
1932+ // 公共前缀求 SHA-256 派生稳定 key:system 提示、工具清单、instructions 稳定
1933+ // 前缀与首个 user message 决定缓存前缀;assistant/tool 后续消息会随轮次变化,
19161934// 不参与哈希。同一份系统提示 + 工具栈在跨 launch 之间生成同一 key,让上游
19171935// 一致性哈希到同一 zen 缓存分片;前缀内容一旦改动则自然换 key,避免串污染。
1936+ // instructions 只取稳定头(见 stableInstructionsHead):实测 Claude Code 每轮
1937+ // 在尾部追加 role=system 消息(59→77 段,+6KB),全量哈希会导致同会话每轮换
1938+ // key、prefix-cache 归零;头 16 段跨轮逐段同哈希,是真正的稳定前缀。
19181939func contentPromptCacheKey (m map [string ]any ) string {
19191940 if m == nil {
19201941 return ""
19211942 }
1922- // 采样:system 块、tools 形状、instructions、首个 user 消息文本——这些
1923- // 是任何 agent 会话的稳定前缀;其它动态字段忽略。
1943+ // 采样:system 块、tools 形状、instructions 稳定头 、首个 user 消息文本——
1944+ // 这些是任何 agent 会话的稳定前缀;其它动态字段忽略。
19241945 parts := []string {}
19251946 if sys , ok := m ["system" ]; ok {
19261947 b , _ := json .Marshal (sys )
19271948 parts = append (parts , "sys:" + string (b ))
19281949 }
19291950 if instr , ok := m ["instructions" ].(string ); ok && instr != "" {
1930- parts = append (parts , "instr:" + instr )
1951+ parts = append (parts , "instr:" + stableInstructionsHead ( instr ) )
19311952 }
19321953 if tools , ok := m ["tools" ].([]any ); ok {
19331954 if b , err := json .Marshal (tools ); err == nil {
@@ -2002,8 +2023,10 @@ func contentPromptCacheKey(m map[string]any) string {
20022023 if len (parts ) == 0 {
20032024 return ""
20042025 }
2026+ sum := sha256 .Sum256 ([]byte (strings .Join (parts , "\n " )))
2027+ pck := "oc2api:csha:" + hex .EncodeToString (sum [:16 ])
20052028 if cacheDebugEnabled () {
2006- attrs := []any {"parts" , len (parts )}
2029+ attrs := []any {"parts" , len (parts ), "pck" , pck }
20072030 for i , p := range parts {
20082031 idx := strings .Index (p , ":" )
20092032 name , body := p , p
@@ -2012,9 +2035,38 @@ func contentPromptCacheKey(m map[string]any) string {
20122035 }
20132036 attrs = append (attrs , fmt .Sprintf ("part%d" , i ), name + "=" + hashTextPrefix (body ))
20142037 attrs = append (attrs , fmt .Sprintf ("part%d_len" , i ), len (body ))
2038+ if name == "instr" {
2039+ // part0 已是稳定头(stableInstructionsHead 截断后),full 长度
2040+ // 需从原始请求体另取:m["instructions"] 即原文。
2041+ if full , _ := m ["instructions" ].(string ); full != "" {
2042+ attrs = append (attrs , "instr_full_len" , len (full ))
2043+ attrs = append (attrs , "instr_full_segs" , len (strings .Split (full , "\n \n " )))
2044+ }
2045+ segs := strings .Split (body , "\n \n " )
2046+ attrs = append (attrs , "instr_segs" , len (segs ))
2047+ // 头 16 + 尾 16:Claude 侧 instructions = system 参数 +
2048+ // role=system 消息 join,膨胀段常在尾部,32 上限截断后看不见。
2049+ // 只记哈希前缀+长度,不记原文。
2050+ emit := func (si int ) {
2051+ attrs = append (attrs , fmt .Sprintf ("instr_seg%d" , si ), hashTextPrefix (segs [si ]))
2052+ attrs = append (attrs , fmt .Sprintf ("instr_seg%d_len" , si ), len (segs [si ]))
2053+ }
2054+ n := len (segs )
2055+ for si := 0 ; si < n && si < 16 ; si ++ {
2056+ emit (si )
2057+ }
2058+ if n > 32 {
2059+ for si := n - 16 ; si < n ; si ++ {
2060+ emit (si )
2061+ }
2062+ } else {
2063+ for si := 16 ; si < n && si < 32 ; si ++ {
2064+ emit (si )
2065+ }
2066+ }
2067+ }
20152068 }
20162069 slog .Info ("cache_debug_key_parts" , attrs ... )
20172070 }
2018- sum := sha256 .Sum256 ([]byte (strings .Join (parts , "\n " )))
2019- return "oc2api:csha:" + hex .EncodeToString (sum [:16 ])
2071+ return pck
20202072}
0 commit comments