gateway_tool_rewrite.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. package service
  2. import (
  3. "fmt"
  4. "hash/fnv"
  5. "math/rand"
  6. "sort"
  7. "strings"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  9. "github.com/tidwall/gjson"
  10. "github.com/tidwall/sjson"
  11. )
  12. // toolNameRewriteKey 是 gin.Context 上存 ToolNameRewrite 映射的 key。
  13. // 请求阶段写入,响应阶段读取,用于 bytes 级逆向还原假名 → 真名。
  14. const toolNameRewriteKey = "claude_tool_name_rewrite"
  15. // staticToolNameRewrites 是"静态前缀映射",与 Parrot src/transform/cc_mimicry.py
  16. // TOOL_NAME_REWRITES 完全一致。只有以这些前缀开头的工具会被重写。
  17. var staticToolNameRewrites = map[string]string{
  18. "sessions_": "cc_sess_",
  19. "session_": "cc_ses_",
  20. }
  21. // fakeToolNamePrefixes 是"动态映射"的前缀池,与 Parrot _FAKE_PREFIXES 一致。
  22. // 当 tools 数量 > dynamicToolMapThreshold 时随机选用其中前缀生成可读假名。
  23. var fakeToolNamePrefixes = []string{
  24. "analyze_", "compute_", "fetch_", "generate_", "lookup_", "modify_",
  25. "process_", "query_", "render_", "resolve_", "sync_", "update_",
  26. "validate_", "convert_", "extract_", "manage_", "monitor_", "parse_",
  27. "review_", "search_", "transform_", "handle_", "invoke_", "notify_",
  28. }
  29. // dynamicToolMapThreshold 与 Parrot 一致:tools 数量超过 5 才启用动态映射。
  30. // 少量工具不需要混淆(一般是 Claude Code 自己的核心工具 bash/edit/read 等)。
  31. const dynamicToolMapThreshold = 5
  32. // ToolNameRewrite 是单次请求内的工具名混淆映射。
  33. // - Forward: real → fake,请求阶段在 body 上应用。
  34. // - Reverse: fake → real,响应阶段对每个 chunk 做 bytes.Replace 还原。
  35. //
  36. // ReverseOrdered 是按假名长度倒序的 (fake, real) 列表,用于防止短假名是长假名的
  37. // 子串时 bytes.Replace 先被吃掉(对齐 Parrot _restore_tool_names_in_chunk 的
  38. // `sorted(..., key=lambda x: len(x[1]), reverse=True)`)。
  39. type ToolNameRewrite struct {
  40. Forward map[string]string
  41. Reverse map[string]string
  42. ReverseOrdered [][2]string
  43. }
  44. // buildDynamicToolMap 构造 tools 的动态假名映射。
  45. //
  46. // 与 Parrot _build_dynamic_tool_map 语义等价:
  47. // - tools 数量 ≤ dynamicToolMapThreshold 时返回 nil(不做动态映射,走静态 fallback)
  48. // - 同一组 tool_names 在同进程内映射稳定(保证 cache 命中)
  49. //
  50. // Parrot 用 `random.Random(hash(tuple(tool_names)))` 作 seed + shuffle 前缀池;
  51. // Go 无法字节级复刻 Python hash,但"稳定性"和"前缀池打散"两个不变量都保留:
  52. // 用 fnv64a(strings.Join(names, "\x00")) 作 seed 喂 math/rand.New。
  53. // 字节级不同不影响上游判定(Anthropic 不会验证我们的随机种子算法)。
  54. func buildDynamicToolMap(toolNames []string) map[string]string {
  55. if len(toolNames) <= dynamicToolMapThreshold {
  56. return nil
  57. }
  58. h := fnv.New64a()
  59. for i, n := range toolNames {
  60. if i > 0 {
  61. _, _ = h.Write([]byte{0})
  62. }
  63. _, _ = h.Write([]byte(n))
  64. }
  65. rng := rand.New(rand.NewSource(int64(h.Sum64())))
  66. available := make([]string, len(fakeToolNamePrefixes))
  67. copy(available, fakeToolNamePrefixes)
  68. rng.Shuffle(len(available), func(i, j int) { available[i], available[j] = available[j], available[i] })
  69. mapping := make(map[string]string, len(toolNames))
  70. for i, name := range toolNames {
  71. prefix := available[i%len(available)]
  72. headLen := 3
  73. if len(name) < 3 {
  74. headLen = len(name)
  75. }
  76. fake := fmt.Sprintf("%s%s%02d", prefix, name[:headLen], i)
  77. mapping[name] = fake
  78. }
  79. return mapping
  80. }
  81. // sanitizeToolName 把真名转成假名。
  82. // 与 Parrot _sanitize_tool_name 语义一致:动态映射优先,再走静态前缀映射。
  83. func sanitizeToolName(name string, dynamic map[string]string) string {
  84. if dynamic != nil {
  85. if fake, ok := dynamic[name]; ok {
  86. return fake
  87. }
  88. }
  89. for prefix, replacement := range staticToolNameRewrites {
  90. if strings.HasPrefix(name, prefix) {
  91. return replacement + name[len(prefix):]
  92. }
  93. }
  94. return name
  95. }
  96. // shouldMimicToolName 指示某个 tool 是否需要重命名。
  97. // server tool(type != "" 且不是 "function" / "custom")是 Anthropic 协议语义的一部分,
  98. // 比如 "web_search_20250305" / "computer_20250124";误改会导致上游拒绝。
  99. func shouldMimicToolName(toolType string) bool {
  100. if toolType == "" || toolType == "function" || toolType == "custom" {
  101. return true
  102. }
  103. return false
  104. }
  105. // buildToolNameRewriteFromBody 扫描 body 的 tools[*].name,构造 ToolNameRewrite
  106. // 并返回它。若不需要混淆(tools 数量不足 + 没有匹配静态前缀的工具)返回 nil。
  107. //
  108. // 注意:只扫描,不改 body。真正的 body 改写在 applyToolNameRewriteToBody。
  109. func buildToolNameRewriteFromBody(body []byte) *ToolNameRewrite {
  110. tools := gjson.GetBytes(body, "tools")
  111. if !tools.IsArray() {
  112. return nil
  113. }
  114. mimicableNames := make([]string, 0)
  115. toolsArr := tools.Array()
  116. for _, t := range toolsArr {
  117. if !shouldMimicToolName(t.Get("type").String()) {
  118. continue
  119. }
  120. name := t.Get("name").String()
  121. if name == "" {
  122. continue
  123. }
  124. mimicableNames = append(mimicableNames, name)
  125. }
  126. dynamic := buildDynamicToolMap(mimicableNames)
  127. rw := &ToolNameRewrite{
  128. Forward: make(map[string]string),
  129. Reverse: make(map[string]string),
  130. }
  131. for _, name := range mimicableNames {
  132. fake := sanitizeToolName(name, dynamic)
  133. if fake == name {
  134. continue
  135. }
  136. rw.Forward[name] = fake
  137. rw.Reverse[fake] = name
  138. }
  139. if len(rw.Forward) == 0 {
  140. return nil
  141. }
  142. rw.ReverseOrdered = make([][2]string, 0, len(rw.Reverse))
  143. for fake, real := range rw.Reverse {
  144. rw.ReverseOrdered = append(rw.ReverseOrdered, [2]string{fake, real})
  145. }
  146. sort.SliceStable(rw.ReverseOrdered, func(i, j int) bool {
  147. return len(rw.ReverseOrdered[i][0]) > len(rw.ReverseOrdered[j][0])
  148. })
  149. return rw
  150. }
  151. // applyToolNameRewriteToBody 把已构造的 ToolNameRewrite 应用到 body 上:
  152. //
  153. // - 改写 $.tools[*].name(仅对 shouldMimicToolName 通过的 tool)
  154. // - 改写 $.tool_choice.name(仅当 $.tool_choice.type == "tool")
  155. // - 改写 $.messages[*].content[*].name(仅当 type == "tool_use")
  156. // - 在 $.tools[last].cache_control 上打 ephemeral 缓存断点
  157. //
  158. // 响应侧 bytes.Replace 会连带还原假名 → 真名。
  159. func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
  160. if rw == nil || len(rw.Forward) == 0 {
  161. body = applyToolsLastCacheBreakpoint(body)
  162. return body
  163. }
  164. tools := gjson.GetBytes(body, "tools")
  165. if tools.IsArray() {
  166. idx := -1
  167. tools.ForEach(func(_, t gjson.Result) bool {
  168. idx++
  169. if !shouldMimicToolName(t.Get("type").String()) {
  170. return true
  171. }
  172. name := t.Get("name").String()
  173. if name == "" {
  174. return true
  175. }
  176. fake, ok := rw.Forward[name]
  177. if !ok {
  178. return true
  179. }
  180. if next, err := sjson.SetBytes(body, fmt.Sprintf("tools.%d.name", idx), fake); err == nil {
  181. body = next
  182. }
  183. return true
  184. })
  185. }
  186. if tc := gjson.GetBytes(body, "tool_choice"); tc.Exists() && tc.Get("type").String() == "tool" {
  187. name := tc.Get("name").String()
  188. if fake, ok := rw.Forward[name]; ok {
  189. if next, err := sjson.SetBytes(body, "tool_choice.name", fake); err == nil {
  190. body = next
  191. }
  192. }
  193. }
  194. // 同步改写历史消息中的 tool_use.name,确保它和 tools[] 中的假名一致。
  195. // 否则 Anthropic 会因为 tool_use 引用了未声明的原始工具名而拒绝请求。
  196. messages := gjson.GetBytes(body, "messages")
  197. if messages.IsArray() {
  198. messages.ForEach(func(msgKey, msg gjson.Result) bool {
  199. msgIdx := int(msgKey.Num)
  200. content := msg.Get("content")
  201. if !content.IsArray() {
  202. return true
  203. }
  204. content.ForEach(func(blkKey, blk gjson.Result) bool {
  205. blkIdx := int(blkKey.Num)
  206. if blk.Get("type").String() != "tool_use" {
  207. return true
  208. }
  209. name := blk.Get("name").String()
  210. if name == "" {
  211. return true
  212. }
  213. if fake, ok := rw.Forward[name]; ok {
  214. path := fmt.Sprintf("messages.%d.content.%d.name", msgIdx, blkIdx)
  215. if next, err := sjson.SetBytes(body, path, fake); err == nil {
  216. body = next
  217. }
  218. }
  219. return true
  220. })
  221. return true
  222. })
  223. }
  224. body = applyToolsLastCacheBreakpoint(body)
  225. return body
  226. }
  227. // applyToolsLastCacheBreakpoint 在最后一个非延迟加载工具上注入 cache_control
  228. // 断点。Anthropic 不允许 defer_loading=true 的工具携带 cache_control,
  229. // 因此会先清理所有延迟加载工具上的客户端断点。兼容官方顶层字段和
  230. // Claude Code 使用的 custom.defer_loading 字段。其余行为对齐 Parrot
  231. // `tools[-1]["cache_control"] = {"type":"ephemeral","ttl":"1h"}`,
  232. // 但 ttl 按本仓规则:
  233. // - 客户端已为该 tool 显式设置 cache_control.ttl → 完全透传不覆盖
  234. // - 否则注入 {"type":"ephemeral","ttl": claude.DefaultCacheControlTTL}
  235. //
  236. // 纯副作用函数,tools 不存在或为空数组时 no-op。
  237. func applyToolsLastCacheBreakpoint(body []byte) []byte {
  238. body = stripDeferredToolCacheControl(body)
  239. tools := gjson.GetBytes(body, "tools")
  240. if !tools.IsArray() {
  241. return body
  242. }
  243. arr := tools.Array()
  244. if len(arr) == 0 {
  245. return body
  246. }
  247. lastIdx := -1
  248. for idx, tool := range arr {
  249. if isDeferredLoadingTool(tool) {
  250. continue
  251. }
  252. lastIdx = idx
  253. }
  254. if lastIdx == -1 {
  255. return body
  256. }
  257. existingCC := arr[lastIdx].Get("cache_control")
  258. if existingCC.Exists() && existingCC.Get("ttl").String() != "" {
  259. return body
  260. }
  261. if existingCC.Exists() {
  262. if next, err := sjson.SetBytes(body, fmt.Sprintf("tools.%d.cache_control.ttl", lastIdx), claude.DefaultCacheControlTTL); err == nil {
  263. body = next
  264. }
  265. return body
  266. }
  267. raw := fmt.Sprintf(`{"type":"ephemeral","ttl":%q}`, claude.DefaultCacheControlTTL)
  268. if next, err := sjson.SetRawBytes(body, fmt.Sprintf("tools.%d.cache_control", lastIdx), []byte(raw)); err == nil {
  269. body = next
  270. }
  271. return body
  272. }
  273. func isDeferredLoadingTool(tool gjson.Result) bool {
  274. return tool.Get("defer_loading").Type == gjson.True ||
  275. tool.Get("custom.defer_loading").Type == gjson.True
  276. }
  277. // stripDeferredToolCacheControl removes the cache marker Anthropic rejects on
  278. // deferred tools. Only the literal JSON boolean true enables deferred loading.
  279. func stripDeferredToolCacheControl(body []byte) []byte {
  280. tools := gjson.GetBytes(body, "tools")
  281. if !tools.IsArray() {
  282. return body
  283. }
  284. for idx, tool := range tools.Array() {
  285. if !isDeferredLoadingTool(tool) || !tool.Get("cache_control").Exists() {
  286. continue
  287. }
  288. if next, err := sjson.DeleteBytes(body, fmt.Sprintf("tools.%d.cache_control", idx)); err == nil {
  289. body = next
  290. }
  291. }
  292. return body
  293. }
  294. // restoreToolNamesInBytes 对 bytes chunk 做逆向还原:假名 → 真名。
  295. // 按 ReverseOrdered 的假名长度倒序逐个 bytes.Replace,防止子串冲突
  296. // (与 Parrot _restore_tool_names_in_chunk 的 sorted(..., reverse=True) 等价)。
  297. // 再做静态前缀还原(cc_sess_ → sessions_ / cc_ses_ → session_)。
  298. //
  299. // rw 可为 nil;nil 时仍会做静态前缀还原。
  300. func restoreToolNamesInBytes(data []byte, rw *ToolNameRewrite) []byte {
  301. if rw != nil {
  302. for _, pair := range rw.ReverseOrdered {
  303. fake, real := pair[0], pair[1]
  304. if fake == "" || fake == real {
  305. continue
  306. }
  307. data = replaceAllBytes(data, fake, real)
  308. }
  309. }
  310. for prefix, replacement := range staticToolNameRewrites {
  311. data = replaceAllBytes(data, replacement, prefix)
  312. }
  313. return data
  314. }
  315. // replaceAllBytes 是 bytes.ReplaceAll 的便捷封装,避免每个调用点各自做 []byte 转换。
  316. func replaceAllBytes(data []byte, from, to string) []byte {
  317. if len(data) == 0 || from == to || !strings.Contains(string(data), from) {
  318. return data
  319. }
  320. return []byte(strings.ReplaceAll(string(data), from, to))
  321. }
  322. // toolNameRewriteFromContext 从 gin.Context 取出请求阶段保存的工具名映射。
  323. // 找不到(c==nil 或 key 不存在或类型不对)时返回 nil;调用方必须能处理 nil。
  324. func toolNameRewriteFromContext(c interface {
  325. Get(string) (any, bool)
  326. }) *ToolNameRewrite {
  327. if c == nil {
  328. return nil
  329. }
  330. raw, ok := c.Get(toolNameRewriteKey)
  331. if !ok || raw == nil {
  332. return nil
  333. }
  334. rw, _ := raw.(*ToolNameRewrite)
  335. return rw
  336. }
  337. // reverseToolNamesIfPresent 是响应侧 5 处注入点的统一封装:从 c 取出 mapping
  338. // 并对 chunk 做 bytes 级假名→真名替换。c 没有 mapping 时仍会做静态前缀还原。
  339. func reverseToolNamesIfPresent(c interface {
  340. Get(string) (any, bool)
  341. }, chunk []byte) []byte {
  342. rw := toolNameRewriteFromContext(c)
  343. if rw == nil && len(staticToolNameRewrites) == 0 {
  344. return chunk
  345. }
  346. return restoreToolNamesInBytes(chunk, rw)
  347. }