gateway_messages_cache.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  7. "github.com/tidwall/gjson"
  8. "github.com/tidwall/sjson"
  9. )
  10. // stripMessageCacheControl 移除 $.messages[*].content[*].cache_control。
  11. // 与 Parrot _strip_message_cache_control 语义一致。
  12. //
  13. // 旧策略为什么整体清空:客户端(特别是 Claude Code)经常把 cache_control 打在
  14. // "当前最后一条 user message" 上;下一轮对话 messages 追加后,原本的最后一条
  15. // 变成中间某条,cache_control 还挂着就导致"前缀签名变化",破坏缓存命中。
  16. // 统一由代理重新打断点(addMessageCacheBreakpoints)才能在多轮间稳定。
  17. func stripMessageCacheControl(body []byte) []byte {
  18. messages := gjson.GetBytes(body, "messages")
  19. if !messages.IsArray() {
  20. return body
  21. }
  22. msgIdx := -1
  23. messages.ForEach(func(_, msg gjson.Result) bool {
  24. msgIdx++
  25. content := msg.Get("content")
  26. if !content.IsArray() {
  27. return true
  28. }
  29. blockIdx := -1
  30. content.ForEach(func(_, block gjson.Result) bool {
  31. blockIdx++
  32. if !block.Get("cache_control").Exists() {
  33. return true
  34. }
  35. path := fmt.Sprintf("messages.%d.content.%d.cache_control", msgIdx, blockIdx)
  36. if next, err := sjson.DeleteBytes(body, path); err == nil {
  37. body = next
  38. }
  39. return true
  40. })
  41. return true
  42. })
  43. return body
  44. }
  45. // addMessageCacheBreakpoints 在 messages 上注入两个稳定的 cache 断点:
  46. // 1. 最后一条 message
  47. // 2. 当 messages 数量 ≥ 4 时,倒数第二个 role=user 的 message
  48. //
  49. // 与 Parrot add_cache_breakpoints 一致。两个断点 + system prompt block 的断点
  50. // + tools[-1] 的断点共同构成最多 4 个断点(Anthropic 上限)。
  51. //
  52. // cache_control ttl 策略:
  53. // - 若目标 block 已有 cache_control.ttl → 不覆盖
  54. // - 否则写入 {"type":"ephemeral","ttl": claude.DefaultCacheControlTTL}
  55. //
  56. // 调用前应先 stripMessageCacheControl 以保证幂等和稳定。
  57. func addMessageCacheBreakpoints(body []byte) []byte {
  58. messages := gjson.GetBytes(body, "messages")
  59. if !messages.IsArray() {
  60. return body
  61. }
  62. arr := messages.Array()
  63. if len(arr) == 0 {
  64. return body
  65. }
  66. body = injectCacheControlOnLastContentBlock(body, len(arr)-1, &arr[len(arr)-1])
  67. if len(arr) >= 4 {
  68. userCount := 0
  69. for i := len(arr) - 1; i >= 0; i-- {
  70. if arr[i].Get("role").String() != "user" {
  71. continue
  72. }
  73. userCount++
  74. if userCount == 2 {
  75. body = injectCacheControlOnLastContentBlock(body, i, &arr[i])
  76. break
  77. }
  78. }
  79. }
  80. return body
  81. }
  82. // rewriteMessageCacheControlIfEnabled 按系统设置决定是否执行旧版 messages 缓存断点改写。
  83. func (s *GatewayService) rewriteMessageCacheControlIfEnabled(ctx context.Context, body []byte) []byte {
  84. if s == nil || !s.isRewriteMessageCacheControlEnabled(ctx) {
  85. return body
  86. }
  87. body = stripMessageCacheControl(body)
  88. return addMessageCacheBreakpoints(body)
  89. }
  90. func (s *GatewayService) isRewriteMessageCacheControlEnabled(ctx context.Context) bool {
  91. if s == nil {
  92. return false
  93. }
  94. if s.settingService != nil {
  95. return s.settingService.IsRewriteMessageCacheControlEnabled(ctx)
  96. }
  97. return false
  98. }
  99. // injectCacheControlOnLastContentBlock 把 cache_control 断点打在 messages[idx]
  100. // 的最后一个 content block 上。若 content 是 string,先升级成单块 text 数组
  101. // (对齐 Parrot _inject_cache_on_msg 的行为)。
  102. //
  103. // msg 是调用方已持有的 gjson.Result 快照,用于省一次 GetBytes。
  104. func injectCacheControlOnLastContentBlock(body []byte, idx int, msg *gjson.Result) []byte {
  105. content := msg.Get("content")
  106. if content.Type == gjson.String {
  107. text := content.String()
  108. blockRaw := fmt.Sprintf(
  109. `[{"type":"text","text":%s,"cache_control":{"type":"ephemeral","ttl":%q}}]`,
  110. mustJSONString(text), claude.DefaultCacheControlTTL,
  111. )
  112. if next, err := sjson.SetRawBytes(body, fmt.Sprintf("messages.%d.content", idx), []byte(blockRaw)); err == nil {
  113. body = next
  114. }
  115. return body
  116. }
  117. if !content.IsArray() {
  118. return body
  119. }
  120. contentArr := content.Array()
  121. if len(contentArr) == 0 {
  122. return body
  123. }
  124. lastBlockIdx := len(contentArr) - 1
  125. lastBlock := contentArr[lastBlockIdx]
  126. if cc := lastBlock.Get("cache_control"); cc.Exists() && cc.Get("ttl").String() != "" {
  127. return body
  128. }
  129. pathPrefix := fmt.Sprintf("messages.%d.content.%d.cache_control", idx, lastBlockIdx)
  130. existingCC := lastBlock.Get("cache_control")
  131. if existingCC.Exists() {
  132. if next, err := sjson.SetBytes(body, pathPrefix+".ttl", claude.DefaultCacheControlTTL); err == nil {
  133. body = next
  134. }
  135. return body
  136. }
  137. raw := fmt.Sprintf(`{"type":"ephemeral","ttl":%q}`, claude.DefaultCacheControlTTL)
  138. if next, err := sjson.SetRawBytes(body, pathPrefix, []byte(raw)); err == nil {
  139. body = next
  140. }
  141. return body
  142. }
  143. // mustJSONString 把一个 Go string 序列化为合法 JSON string(含引号),
  144. // 用于 sjson.SetRawBytes 场景下手工拼 JSON。
  145. func mustJSONString(s string) string {
  146. // Go string quoting can emit non-JSON escapes such as \x7f or \a.
  147. // Marshaling a string cannot fail.
  148. encoded, _ := json.Marshal(s)
  149. return string(encoded)
  150. }