tianshu-harness 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/LICENSE +189 -0
  2. package/README.en.md +1066 -0
  3. package/README.ja.md +1214 -0
  4. package/README.ko.md +1216 -0
  5. package/README.md +309 -0
  6. package/completions/rivet.bash +118 -0
  7. package/completions/rivet.fish +110 -0
  8. package/completions/rivet.ps1 +230 -0
  9. package/completions/rivet.zsh +187 -0
  10. package/dist/account-OAEKORZE.js +47 -0
  11. package/dist/account-login-V6FOLHJR.js +65 -0
  12. package/dist/account-status-KHNPPAFL.js +23 -0
  13. package/dist/agent/worker-process/child.js +407 -0
  14. package/dist/browser-cli-FLNY5ZHI.js +19 -0
  15. package/dist/browser-readiness-LWVGSBD5.js +12 -0
  16. package/dist/build-backends-5KXKR2S4.js +13 -0
  17. package/dist/build-options-66Y4ZE7C.js +9 -0
  18. package/dist/bundled-skills/brainstorming/SKILL.md +139 -0
  19. package/dist/bundled-skills/visual-acceptance/SKILL.md +64 -0
  20. package/dist/chain-7MAMFXZ3.js +10 -0
  21. package/dist/checkpoint-MND4DUIX.js +33 -0
  22. package/dist/chunk-2E6CK2RC.js +392 -0
  23. package/dist/chunk-2EBWGAXD.js +51 -0
  24. package/dist/chunk-2HDP7RMR.js +119 -0
  25. package/dist/chunk-2HKD2UFM.js +1000 -0
  26. package/dist/chunk-2VYRLULP.js +51 -0
  27. package/dist/chunk-2XN7PM4N.js +125 -0
  28. package/dist/chunk-35H7GVNR.js +66 -0
  29. package/dist/chunk-3C3R7JXY.js +77 -0
  30. package/dist/chunk-3VGD2FXB.js +118 -0
  31. package/dist/chunk-424AP7FM.js +534 -0
  32. package/dist/chunk-4J5TZGOE.js +3707 -0
  33. package/dist/chunk-4QJ3Y5JM.js +898 -0
  34. package/dist/chunk-4U2ET7ZY.js +1326 -0
  35. package/dist/chunk-4XPFLICE.js +876 -0
  36. package/dist/chunk-5B2OCU5P.js +31707 -0
  37. package/dist/chunk-5GGOGLSN.js +73 -0
  38. package/dist/chunk-5JRZPLI6.js +251 -0
  39. package/dist/chunk-5TQ3JYBU.js +397 -0
  40. package/dist/chunk-66JPZSGX.js +55 -0
  41. package/dist/chunk-6HSVXHCZ.js +764 -0
  42. package/dist/chunk-6S5FPUS7.js +21 -0
  43. package/dist/chunk-722LQO64.js +131 -0
  44. package/dist/chunk-73NJ7IT7.js +354 -0
  45. package/dist/chunk-7FW3I7WC.js +95 -0
  46. package/dist/chunk-7KUCBNLB.js +173 -0
  47. package/dist/chunk-7RDOV3DF.js +201 -0
  48. package/dist/chunk-A5MNH4ER.js +79 -0
  49. package/dist/chunk-ADIVZ2D5.js +15 -0
  50. package/dist/chunk-AE3P3F6W.js +366 -0
  51. package/dist/chunk-AERM3VGP.js +280 -0
  52. package/dist/chunk-AEWHC4WM.js +246 -0
  53. package/dist/chunk-AOVQ7VIF.js +169 -0
  54. package/dist/chunk-AOYHP3WI.js +12411 -0
  55. package/dist/chunk-B3YEBABJ.js +7160 -0
  56. package/dist/chunk-BCQXEZPJ.js +154 -0
  57. package/dist/chunk-BKTQAYO2.js +418 -0
  58. package/dist/chunk-BSECIPAQ.js +80 -0
  59. package/dist/chunk-BUIMGNDD.js +52 -0
  60. package/dist/chunk-BXHV3GSB.js +10320 -0
  61. package/dist/chunk-C6ZVZCPP.js +139 -0
  62. package/dist/chunk-CEZNQVWM.js +82 -0
  63. package/dist/chunk-CGFZGR7G.js +1492 -0
  64. package/dist/chunk-CGOADM2X.js +337 -0
  65. package/dist/chunk-CI4R6P2B.js +24 -0
  66. package/dist/chunk-D2TCGRSX.js +73 -0
  67. package/dist/chunk-D4PTYFOG.js +154 -0
  68. package/dist/chunk-D54ZWG4T.js +717 -0
  69. package/dist/chunk-DEBI37QB.js +145 -0
  70. package/dist/chunk-DWROHPXP.js +1051 -0
  71. package/dist/chunk-DZ6UOJ5D.js +92 -0
  72. package/dist/chunk-EBJTVRQN.js +81 -0
  73. package/dist/chunk-EGEI2RQA.js +1167 -0
  74. package/dist/chunk-EHHUASO5.js +1305 -0
  75. package/dist/chunk-EPKTIOED.js +41 -0
  76. package/dist/chunk-ER5JW2QZ.js +5681 -0
  77. package/dist/chunk-ETHFACAB.js +315 -0
  78. package/dist/chunk-EXUE5W2F.js +112 -0
  79. package/dist/chunk-EXXICUIW.js +73 -0
  80. package/dist/chunk-FFQSZJOO.js +169 -0
  81. package/dist/chunk-FHFKPGDO.js +85 -0
  82. package/dist/chunk-FW3QKMFX.js +158 -0
  83. package/dist/chunk-GD4ZNZ7W.js +30 -0
  84. package/dist/chunk-GHVMWF2I.js +581 -0
  85. package/dist/chunk-GMKAXSYQ.js +297 -0
  86. package/dist/chunk-HBVF6BWO.js +325 -0
  87. package/dist/chunk-HV4YGVMS.js +379 -0
  88. package/dist/chunk-ILBCKAV6.js +89 -0
  89. package/dist/chunk-J4U4AW46.js +440 -0
  90. package/dist/chunk-J6ESSM47.js +1791 -0
  91. package/dist/chunk-JBZHIC6T.js +2309 -0
  92. package/dist/chunk-JCFWOULR.js +245 -0
  93. package/dist/chunk-JRF5L6OD.js +131 -0
  94. package/dist/chunk-JSS75KCG.js +2014 -0
  95. package/dist/chunk-JSXWIWWJ.js +225 -0
  96. package/dist/chunk-JYV7I2QH.js +98 -0
  97. package/dist/chunk-KIOXTQQL.js +1596 -0
  98. package/dist/chunk-KWVTXT5B.js +89 -0
  99. package/dist/chunk-KX22RD6R.js +33 -0
  100. package/dist/chunk-LA6IPTGH.js +709 -0
  101. package/dist/chunk-LH5VCGLB.js +1796 -0
  102. package/dist/chunk-LQQ6XJ2Y.js +269 -0
  103. package/dist/chunk-MJQ55WZZ.js +65 -0
  104. package/dist/chunk-ML3UKMH3.js +40 -0
  105. package/dist/chunk-MOZPLHSU.js +5526 -0
  106. package/dist/chunk-NGEWJ4DF.js +89 -0
  107. package/dist/chunk-NH6VIOAR.js +211 -0
  108. package/dist/chunk-O5VZQKCB.js +26106 -0
  109. package/dist/chunk-OHSV42QB.js +748 -0
  110. package/dist/chunk-OO3TAHO5.js +4070 -0
  111. package/dist/chunk-OTNHSAGN.js +38282 -0
  112. package/dist/chunk-OUWAC7AQ.js +442 -0
  113. package/dist/chunk-OXMIV4WE.js +270 -0
  114. package/dist/chunk-P5KJURUR.js +229 -0
  115. package/dist/chunk-P6GJOVO6.js +307 -0
  116. package/dist/chunk-QARAGUXX.js +23 -0
  117. package/dist/chunk-R43HJCDX.js +2711 -0
  118. package/dist/chunk-RAXBAWRN.js +210 -0
  119. package/dist/chunk-RK3JHWPT.js +39 -0
  120. package/dist/chunk-RWA23GMM.js +24 -0
  121. package/dist/chunk-S4K4L6IM.js +179 -0
  122. package/dist/chunk-SAXK6LVF.js +124 -0
  123. package/dist/chunk-SEPGEJNK.js +1607 -0
  124. package/dist/chunk-SUJERP5I.js +285 -0
  125. package/dist/chunk-T3ED6BY2.js +913 -0
  126. package/dist/chunk-THKDVSEC.js +90 -0
  127. package/dist/chunk-TI4GSEL5.js +52 -0
  128. package/dist/chunk-TPI73L57.js +164 -0
  129. package/dist/chunk-TVHW77C7.js +338 -0
  130. package/dist/chunk-TWNHOSDT.js +1073 -0
  131. package/dist/chunk-UFMN76QD.js +347 -0
  132. package/dist/chunk-UHGLFRZE.js +205 -0
  133. package/dist/chunk-UTZYPKBF.js +15231 -0
  134. package/dist/chunk-UXNLELW6.js +25 -0
  135. package/dist/chunk-UXXS53EA.js +166 -0
  136. package/dist/chunk-V5DHFGI7.js +171 -0
  137. package/dist/chunk-V5YIPMHV.js +2353 -0
  138. package/dist/chunk-WMUWEO2C.js +754 -0
  139. package/dist/chunk-WOE3D7JM.js +945 -0
  140. package/dist/chunk-X2IK5LVR.js +2592 -0
  141. package/dist/chunk-XU2YXCDL.js +459 -0
  142. package/dist/chunk-YJG5HIX5.js +203 -0
  143. package/dist/chunk-Z3QE7TZM.js +72 -0
  144. package/dist/chunk-Z5Z3JHJY.js +64 -0
  145. package/dist/chunk-ZDIFBJFT.js +168 -0
  146. package/dist/chunk-ZGGCEXDC.js +176 -0
  147. package/dist/chunk-ZMZCZNKU.js +263 -0
  148. package/dist/chunk-ZNC5T65N.js +17 -0
  149. package/dist/chunk-ZO3I3A7O.js +308 -0
  150. package/dist/chunk-ZQKXBUB7.js +217 -0
  151. package/dist/chunk-ZTRINFM2.js +78 -0
  152. package/dist/chunk-ZU7JZCVP.js +193 -0
  153. package/dist/chunk-ZZAWNALS.js +350 -0
  154. package/dist/cli/entry.js +54 -0
  155. package/dist/connector-L5QQTR6Y.js +26 -0
  156. package/dist/context-62DC6EC3.js +31 -0
  157. package/dist/council-convene-NJWPNBDU.js +46 -0
  158. package/dist/create-agent-config-ODSQWNVC.js +55 -0
  159. package/dist/default-registry-2PU3BTAU.js +68 -0
  160. package/dist/deliver-task-KONV4C4M.js +57 -0
  161. package/dist/delivery-gate-v2-F2YVMVGK.js +27 -0
  162. package/dist/domain-knowledge-store-DDZ3F4J7.js +11 -0
  163. package/dist/fetch-core-ZPPYDSZH.js +14 -0
  164. package/dist/galaxy-K72BBDQN.js +91 -0
  165. package/dist/goal-criteria-SCALAU2D.js +141 -0
  166. package/dist/goal-persist-XWY2GDQX.js +19 -0
  167. package/dist/goal-tracker-FXMJ2GVK.js +11 -0
  168. package/dist/headless-LSB6I7PT.js +162 -0
  169. package/dist/headless-coordinator-VDPDMVUT.js +90 -0
  170. package/dist/inject-U55SMB6A.js +39 -0
  171. package/dist/login-flow-CD4ZGXOC.js +59 -0
  172. package/dist/logs-cli-SQVYFMMN.js +283 -0
  173. package/dist/loop-ULZYBDA3.js +76 -0
  174. package/dist/main.js +4064 -0
  175. package/dist/manager-ROZBCWPA.js +171 -0
  176. package/dist/manager-Z7XFFUOM.js +10670 -0
  177. package/dist/meridian-indexer-ZIW363L7.js +21 -0
  178. package/dist/native-resolver-PN34UIVV.js +13 -0
  179. package/dist/node_modules/better-sqlite3/lib/database.js +90 -0
  180. package/dist/node_modules/better-sqlite3/lib/index.js +3 -0
  181. package/dist/node_modules/better-sqlite3/lib/methods/aggregate.js +43 -0
  182. package/dist/node_modules/better-sqlite3/lib/methods/backup.js +67 -0
  183. package/dist/node_modules/better-sqlite3/lib/methods/function.js +31 -0
  184. package/dist/node_modules/better-sqlite3/lib/methods/inspect.js +7 -0
  185. package/dist/node_modules/better-sqlite3/lib/methods/pragma.js +12 -0
  186. package/dist/node_modules/better-sqlite3/lib/methods/serialize.js +16 -0
  187. package/dist/node_modules/better-sqlite3/lib/methods/table.js +189 -0
  188. package/dist/node_modules/better-sqlite3/lib/methods/transaction.js +78 -0
  189. package/dist/node_modules/better-sqlite3/lib/methods/wrappers.js +54 -0
  190. package/dist/node_modules/better-sqlite3/lib/sqlite-error.js +20 -0
  191. package/dist/node_modules/better-sqlite3/lib/util.js +12 -0
  192. package/dist/node_modules/better-sqlite3/package.json +59 -0
  193. package/dist/ownership-ledger-E3WIEKPL.js +9 -0
  194. package/dist/package.json +3 -0
  195. package/dist/path-grants-XRRXHXG7.js +40 -0
  196. package/dist/paths-LVV5IMQN.js +49 -0
  197. package/dist/plan-fact-anchors-LMVQZCW5.js +13 -0
  198. package/dist/plan-store-ODLTTPFB.js +17 -0
  199. package/dist/plan-templates-5FPFBOL6.js +109 -0
  200. package/dist/plugin-loader-ZZKX7ZE6.js +22 -0
  201. package/dist/prefix-budget-62OV5HQC.js +13 -0
  202. package/dist/project-trust-5WS5MOMU.js +31 -0
  203. package/dist/provider-cli-ALDQXIT7.js +37 -0
  204. package/dist/providers-XQYTAVPK.js +11 -0
  205. package/dist/proxy-resolver-V2Y6534D.js +15 -0
  206. package/dist/recovery-cli-KPGY6FY3.js +106 -0
  207. package/dist/review-discipline-MNSYCST5.js +55 -0
  208. package/dist/review-router-BVQZW3MX.js +18 -0
  209. package/dist/seed-capsules/seed-capsule-attack-methodology.md +62 -0
  210. package/dist/seed-capsules/seed-capsule-changgeng.md +31 -0
  211. package/dist/seed-capsules/seed-capsule-design-aesthetics.md +38 -0
  212. package/dist/seed-capsules/seed-capsule-diagnostic-ladder.md +54 -0
  213. package/dist/seed-capsules/seed-capsule-fu.md +39 -0
  214. package/dist/seed-capsules/seed-capsule-huagai.md +73 -0
  215. package/dist/seed-capsules/seed-capsule-kaiyang.md +65 -0
  216. package/dist/seed-capsules/seed-capsule-knowledge-work.md +40 -0
  217. package/dist/seed-capsules/seed-capsule-tanlang.md +47 -0
  218. package/dist/seed-capsules/seed-capsule-tianfu.md +23 -0
  219. package/dist/seed-capsules/seed-capsule-tianliang.md +63 -0
  220. package/dist/seed-capsules/seed-capsule-tianquan.md +32 -0
  221. package/dist/seed-capsules/seed-capsule-tianxuan-ccr.md +22 -0
  222. package/dist/seed-capsules/seed-capsule-tianxuan.md +64 -0
  223. package/dist/seed-capsules/seed-capsule-yaoguang.md +87 -0
  224. package/dist/serve-X2R2UTLF.js +170 -0
  225. package/dist/serve-agent-KHSU6RVV.js +965 -0
  226. package/dist/session-persist-HUUC772H.js +37 -0
  227. package/dist/session-registry-6XUD5C4Y.js +15 -0
  228. package/dist/skill-loader-CK3RMO47.js +48 -0
  229. package/dist/skills/cli-harness-sop.md +207 -0
  230. package/dist/spawn-git-4HFU65AH.js +29 -0
  231. package/dist/sse-EYUTBWUK.js +512 -0
  232. package/dist/starflow-NOFBMPFU.js +30 -0
  233. package/dist/streamableHttp-V67CNJMO.js +447 -0
  234. package/dist/task-contract-6XHBNJQK.js +32 -0
  235. package/dist/task-ledger-3K4TJIER.js +9 -0
  236. package/dist/team-orchestrate-S6NHZP7X.js +64 -0
  237. package/dist/title-extract-QX4XGCYN.js +50 -0
  238. package/dist/todo-QDXGRZZP.js +31 -0
  239. package/dist/todo-store-V3TTO5AH.js +12 -0
  240. package/dist/tool-preset-LU5BYEDZ.js +23 -0
  241. package/dist/tool-tiers-GD742YBN.js +23 -0
  242. package/dist/turndown.es-OI55K6C3.js +17373 -0
  243. package/dist/verification-attribution-EKG4CNUV.js +13 -0
  244. package/dist/volatile-git-MBTST7RA.js +15 -0
  245. package/dist/wave-checkpoint-XLIIHXQA.js +30 -0
  246. package/dist/web-cli-DUG4I35O.js +233 -0
  247. package/dist/web-search-3IK542AA.js +34 -0
  248. package/dist/workers/cpu-worker.js +45 -0
  249. package/dist/workflow-runner-HX7UBAYG.js +297 -0
  250. package/dist/worktree-baseline-O5NCU6M2.js +9 -0
  251. package/package.json +122 -0
  252. package/scripts/check-mobile-env.mjs +76 -0
  253. package/scripts/fetch-native-sqlite.js +392 -0
  254. package/scripts/prune-installed-wasms.js +45 -0
  255. package/scripts/tree-sitter-wasm-keep.js +40 -0
@@ -0,0 +1,2592 @@
1
+ #!/usr/bin/env -S node --expose-gc --max-old-space-size=4096
2
+ import { createRequire as __rivetCreateRequire } from 'node:module'; const require = __rivetCreateRequire(import.meta.url);
3
+ import {
4
+ sanitizeMessageContent
5
+ } from "./chunk-CEZNQVWM.js";
6
+ import {
7
+ require_undici
8
+ } from "./chunk-O5VZQKCB.js";
9
+ import {
10
+ abortableDelay,
11
+ getProviderProfile,
12
+ withStructuredRetry
13
+ } from "./chunk-ZQKXBUB7.js";
14
+ import {
15
+ parseJsonObjectWithEscapeRepair,
16
+ repairInvalidJsonEscapes
17
+ } from "./chunk-Z5Z3JHJY.js";
18
+ import {
19
+ estimateOaiTokens
20
+ } from "./chunk-KIOXTQQL.js";
21
+ import {
22
+ debugLog
23
+ } from "./chunk-CI4R6P2B.js";
24
+ import {
25
+ normalizeOaiMessage,
26
+ oaiMessagesHaveImageParts,
27
+ stripOaiImageParts
28
+ } from "./chunk-EXXICUIW.js";
29
+ import {
30
+ stableStringify
31
+ } from "./chunk-QARAGUXX.js";
32
+ import {
33
+ contractModels,
34
+ isKeylessProviderEntry,
35
+ normalizeBaseUrl,
36
+ proRegistry,
37
+ readSecret
38
+ } from "./chunk-ER5JW2QZ.js";
39
+ import {
40
+ ReasoningRepetitionGuard,
41
+ fetchCauseDetail,
42
+ parseRetryAfterMs
43
+ } from "./chunk-BKTQAYO2.js";
44
+ import {
45
+ external_exports
46
+ } from "./chunk-OO3TAHO5.js";
47
+ import {
48
+ __toESM,
49
+ init_esm_shims
50
+ } from "./chunk-5GGOGLSN.js";
51
+
52
+ // src/api/provider.ts
53
+ init_esm_shims();
54
+ function mapDeepSeekUsage(raw) {
55
+ return {
56
+ // Support both DeepSeek native format and Anthropic compatibility format
57
+ input_tokens: raw.prompt_tokens ?? raw.input_tokens ?? 0,
58
+ output_tokens: raw.completion_tokens ?? raw.output_tokens ?? 0,
59
+ cache_read_input_tokens: raw.prompt_cache_hit_tokens ?? raw.cache_read_input_tokens ?? raw.prompt_tokens_details?.cached_tokens ?? 0,
60
+ cache_creation_input_tokens: raw.prompt_cache_miss_tokens ?? raw.cache_creation_input_tokens ?? 0
61
+ };
62
+ }
63
+ var DEEPSEEK_CAPABILITIES = {
64
+ supportsThinking: true,
65
+ thinkingBlockType: "enabled",
66
+ preservedThinkingProtocol: true,
67
+ supportsCacheControl: false,
68
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
69
+ hasToolJsonInContentBug: true,
70
+ effortFormat: "reasoning_effort",
71
+ prefixCacheStrategy: "deepseek-native",
72
+ supportsResponseFormat: true,
73
+ mapUsage: mapDeepSeekUsage
74
+ };
75
+ var DEFAULT_CAPABILITIES = {
76
+ supportsThinking: false,
77
+ thinkingBlockType: "none",
78
+ supportsCacheControl: true,
79
+ stripParams: [],
80
+ hasToolJsonInContentBug: false,
81
+ effortFormat: "none",
82
+ prefixCacheStrategy: "none",
83
+ supportsResponseFormat: false
84
+ };
85
+ var WELL_KNOWN_DEFAULTS = {
86
+ deepseek: DEEPSEEK_CAPABILITIES,
87
+ kimi: {
88
+ supportsThinking: true,
89
+ thinkingBlockType: "enabled",
90
+ // 无 effortCap:官方 Kimi Code 文档(kimi-code/models.html)——k3 / k3-256k
91
+ // 的 reasoning_effort 支持 low|high|max,第三方工具传 max 即映射到 max。
92
+ // 旧值 {max:'high'} 会把用户显式选的 max 静默降成 high,K3 旗舰的 max 档
93
+ // 永远发不出去。K2.7 Code(kimi-for-coding)是 Thinking:ON、无档位。
94
+ supportsCacheControl: false,
95
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
96
+ hasToolJsonInContentBug: false,
97
+ effortFormat: "reasoning_effort",
98
+ prefixCacheStrategy: "none",
99
+ supportsResponseFormat: false
100
+ },
101
+ glm: {
102
+ supportsThinking: true,
103
+ thinkingBlockType: "enabled",
104
+ supportsCacheControl: false,
105
+ stripParams: ["top_k", "metadata", "service_tier"],
106
+ hasToolJsonInContentBug: false,
107
+ effortFormat: "reasoning_effort",
108
+ // GLM-5.2 has implicit exact-prefix caching (no cache_control breakpoints),
109
+ // reported via usage.prompt_tokens_details.cached_tokens — same model as DeepSeek.
110
+ prefixCacheStrategy: "deepseek-native",
111
+ supportsResponseFormat: true,
112
+ mapUsage: mapDeepSeekUsage
113
+ },
114
+ minimax: {
115
+ supportsThinking: true,
116
+ thinkingBlockType: "adaptive",
117
+ reasoningSplit: true,
118
+ supportsCacheControl: false,
119
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
120
+ hasToolJsonInContentBug: false,
121
+ effortFormat: "none",
122
+ prefixCacheStrategy: "none",
123
+ supportsResponseFormat: false
124
+ },
125
+ mimo: {
126
+ supportsThinking: true,
127
+ thinkingBlockType: "enabled",
128
+ preservedThinkingProtocol: true,
129
+ supportsCacheControl: false,
130
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
131
+ hasToolJsonInContentBug: false,
132
+ effortFormat: "none",
133
+ prefixCacheStrategy: "none",
134
+ supportsResponseFormat: false
135
+ },
136
+ "mimo-api": {
137
+ supportsThinking: true,
138
+ thinkingBlockType: "enabled",
139
+ supportsCacheControl: false,
140
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
141
+ hasToolJsonInContentBug: false,
142
+ effortFormat: "none",
143
+ prefixCacheStrategy: "deepseek-native",
144
+ supportsResponseFormat: false
145
+ },
146
+ "opencode-go": {
147
+ // 推理透传:上游不认 thinking 块(默认就返回 reasoning_content),但接受
148
+ // reasoning_effort——low/medium/high/max/xhigh 实测全部 200。曾配成
149
+ // effortFormat 'none',而 openai-client 的两个分支都以 effortFormat !== 'none'
150
+ // 为写入前提(openai-client.ts:518-548),于是用户的档位选择被静默吞掉。
151
+ // 透传后是否按档位调节由上游决定;天枢的职责是别把它丢掉。
152
+ supportsThinking: true,
153
+ thinkingBlockType: "none",
154
+ supportsCacheControl: false,
155
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
156
+ hasToolJsonInContentBug: false,
157
+ effortFormat: "reasoning_effort",
158
+ prefixCacheStrategy: "none",
159
+ supportsResponseFormat: false
160
+ },
161
+ openai: {
162
+ supportsThinking: true,
163
+ thinkingBlockType: "none",
164
+ supportsCacheControl: true,
165
+ stripParams: [],
166
+ hasToolJsonInContentBug: false,
167
+ effortFormat: "reasoning_effort",
168
+ prefixCacheStrategy: "none",
169
+ supportsResponseFormat: true
170
+ },
171
+ codex: {
172
+ supportsThinking: true,
173
+ thinkingBlockType: "none",
174
+ effortCap: { max: "xhigh" },
175
+ supportsCacheControl: true,
176
+ stripParams: [],
177
+ hasToolJsonInContentBug: false,
178
+ effortFormat: "reasoning_effort",
179
+ prefixCacheStrategy: "none",
180
+ supportsResponseFormat: true
181
+ },
182
+ claude: {
183
+ supportsThinking: true,
184
+ thinkingBlockType: "enabled",
185
+ thinkingBudgetField: "budget_tokens",
186
+ supportsCacheControl: false,
187
+ stripParams: ["top_k", "metadata", "service_tier"],
188
+ hasToolJsonInContentBug: false,
189
+ effortFormat: "reasoning_effort",
190
+ prefixCacheStrategy: "none",
191
+ supportsResponseFormat: false
192
+ },
193
+ // LongCat official docs support ONLY model/messages/stream/max_tokens/
194
+ // temperature/top_p — no response_format (json-mode repair unusable, worker
195
+ // repair must run as plain-text re-ask) and no cache_control breakpoints
196
+ // (server-side implicit prefix caching, hits free). Explicit entry so
197
+ // behavior doesn't ride on the DEFAULT_CAPABILITIES fallback
198
+ // (session 2c1186f5 scout postmortem).
199
+ longcat: {
200
+ supportsThinking: false,
201
+ thinkingBlockType: "none",
202
+ supportsCacheControl: false,
203
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
204
+ hasToolJsonInContentBug: false,
205
+ effortFormat: "none",
206
+ prefixCacheStrategy: "deepseek-native",
207
+ supportsResponseFormat: false,
208
+ mapUsage: mapDeepSeekUsage
209
+ },
210
+ ccswitch: {
211
+ // cc-switch 是 OpenAI 兼容代理,入口层透传 reasoning_effort;
212
+ // 其 Rectifier 翻译层会将 OpenAI 格式转为 Claude/DeepSeek 等上游原生格式。
213
+ // 后端模型不认识 reasoning_effort 时按 OpenAI 兼容约定静默忽略(降级)。
214
+ supportsThinking: true,
215
+ thinkingBlockType: "none",
216
+ supportsCacheControl: false,
217
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
218
+ hasToolJsonInContentBug: false,
219
+ effortFormat: "reasoning_effort",
220
+ prefixCacheStrategy: "none",
221
+ supportsResponseFormat: false
222
+ },
223
+ // ── Aggregator / relay providers ─────────────────────────────────────────
224
+ // SiliconFlow aggregator (硅基流动). Default model in preset is DeepSeek-proxied
225
+ // → toolJsonBug:true set in preset overrides. Server-side implicit prefix caching
226
+ // on DeepSeek-V4 / GLM-5.2 (charges for cached input) → deepseek-native strategy
227
+ // to preserve cache-aware compaction.
228
+ siliconflow: {
229
+ supportsThinking: true,
230
+ thinkingBlockType: "enabled",
231
+ supportsCacheControl: false,
232
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
233
+ hasToolJsonInContentBug: false,
234
+ effortFormat: "reasoning_effort",
235
+ prefixCacheStrategy: "deepseek-native",
236
+ supportsResponseFormat: false,
237
+ mapUsage: mapDeepSeekUsage
238
+ },
239
+ // DashScope (阿里通义千问官方 OpenAI 兼容端点). Qwen3-max supports thinking
240
+ // block; Qwen-plus/turbo do not — per-model override via `models[].capabilities`.
241
+ // DashScope OpenAI-compatible endpoint does not accept cache_control breakpoints
242
+ // (that's Anthropic protocol); cache profile is 'explicit-breakpoint' only via
243
+ // the legacy PROFILES['qwen'] entry kept for back-compat.
244
+ dashscope: {
245
+ supportsThinking: true,
246
+ thinkingBlockType: "enabled",
247
+ supportsCacheControl: false,
248
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
249
+ hasToolJsonInContentBug: false,
250
+ effortFormat: "reasoning_effort",
251
+ prefixCacheStrategy: "none",
252
+ supportsResponseFormat: true
253
+ },
254
+ // OpenRouter international aggregator. thinking block passthrough is unstable
255
+ // across the model fleet → thinkingBlockType:'none', rely on reasoning_effort
256
+ // passthrough only. Users can override per-model via `models[].capabilities`.
257
+ openrouter: {
258
+ supportsThinking: true,
259
+ thinkingBlockType: "none",
260
+ supportsCacheControl: false,
261
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
262
+ hasToolJsonInContentBug: false,
263
+ effortFormat: "reasoning_effort",
264
+ prefixCacheStrategy: "none",
265
+ supportsResponseFormat: false
266
+ },
267
+ // one-api / new-api self-hosted relay (generic template, not activated by
268
+ // default to avoid overlap with ccswitch). Same shape as ccswitch — relay
269
+ // entry point passes reasoning_effort through, upstream Rectifier translates.
270
+ relay: {
271
+ supportsThinking: true,
272
+ thinkingBlockType: "none",
273
+ supportsCacheControl: false,
274
+ stripParams: ["top_k", "metadata", "service_tier", "cache_control"],
275
+ hasToolJsonInContentBug: false,
276
+ effortFormat: "reasoning_effort",
277
+ prefixCacheStrategy: "none",
278
+ supportsResponseFormat: false
279
+ }
280
+ };
281
+ function applyOverrides(base, overrides) {
282
+ if (!overrides) return base;
283
+ if (overrides.cacheControl !== void 0) base.supportsCacheControl = overrides.cacheControl;
284
+ if (overrides.stripParams !== void 0 && overrides.stripParams.length > 0) base.stripParams = overrides.stripParams;
285
+ if (overrides.toolJsonBug !== void 0) base.hasToolJsonInContentBug = overrides.toolJsonBug;
286
+ if (overrides.prefixCache !== void 0) base.prefixCacheStrategy = overrides.prefixCache;
287
+ if (overrides.thinkingBlock !== void 0) base.thinkingBlockType = overrides.thinkingBlock;
288
+ if (overrides.effortFormat !== void 0) base.effortFormat = overrides.effortFormat;
289
+ if (overrides.effortCap !== void 0) base.effortCap = { ...overrides.effortCap };
290
+ if (overrides.reasoningSplit !== void 0) base.reasoningSplit = overrides.reasoningSplit;
291
+ if (overrides.thinkingBudgetField !== void 0) base.thinkingBudgetField = overrides.thinkingBudgetField;
292
+ if (overrides.preservedThinkingProtocol !== void 0) base.preservedThinkingProtocol = overrides.preservedThinkingProtocol;
293
+ const declaresThinking = overrides.thinkingBlock === "enabled" || overrides.thinkingBlock === "adaptive" || overrides.effortFormat === "reasoning_effort" || overrides.effortFormat === "output_config";
294
+ const declaresNoThinking = overrides.thinkingBlock === "none" && overrides.effortFormat === "none";
295
+ if (declaresThinking) base.supportsThinking = true;
296
+ else if (declaresNoThinking) base.supportsThinking = false;
297
+ return base;
298
+ }
299
+ function resolveCapabilities(providerName, providerOverrides, modelOverrides) {
300
+ const base = {
301
+ ...WELL_KNOWN_DEFAULTS[providerName] ?? DEFAULT_CAPABILITIES
302
+ };
303
+ applyOverrides(base, providerOverrides);
304
+ applyOverrides(base, modelOverrides);
305
+ return base;
306
+ }
307
+
308
+ // src/api/factory.ts
309
+ init_esm_shims();
310
+
311
+ // src/api/openai-client.ts
312
+ init_esm_shims();
313
+
314
+ // src/api/fetch-timeout.ts
315
+ init_esm_shims();
316
+ var DEFAULT_TIMEOUT_MS = 45e3;
317
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, dispatcher) {
318
+ const userSignal = init.signal;
319
+ const timeoutController = new AbortController();
320
+ let timedOut = false;
321
+ const timer = setTimeout(() => {
322
+ timedOut = true;
323
+ timeoutController.abort();
324
+ }, timeoutMs);
325
+ const combinedSignal = userSignal ? AbortSignal.any([userSignal, timeoutController.signal]) : timeoutController.signal;
326
+ try {
327
+ const redirect = init.redirect ?? "error";
328
+ return await fetch(url, { ...init, redirect, signal: combinedSignal, ...dispatcher ? { dispatcher } : {} });
329
+ } catch (err) {
330
+ const name = err.name;
331
+ if (timedOut || name === "TimeoutError") {
332
+ throw new Error(
333
+ `Request timed out: server did not respond within ${Math.round(timeoutMs / 1e3)} seconds`
334
+ );
335
+ }
336
+ if (name === "AbortError" || userSignal?.aborted) throw err;
337
+ if (err instanceof Error && /fetch failed/i.test(err.message)) {
338
+ const detail = fetchCauseDetail(err);
339
+ if (detail) throw new Error(`${err.message}: ${detail}`, { cause: err });
340
+ }
341
+ throw err;
342
+ } finally {
343
+ clearTimeout(timer);
344
+ }
345
+ }
346
+
347
+ // src/api/abort-reader.ts
348
+ init_esm_shims();
349
+ function wireAbortToReaderCancel(signal, reader) {
350
+ const onAbort = () => reader.cancel().catch(() => {
351
+ });
352
+ if (signal.aborted) {
353
+ reader.cancel().catch(() => {
354
+ });
355
+ return () => {
356
+ };
357
+ }
358
+ signal.addEventListener("abort", onAbort, { once: true });
359
+ return () => signal.removeEventListener("abort", onAbort);
360
+ }
361
+ function wrapBodyTimeoutError(err, provider, streamStartedAt) {
362
+ const name = err?.name;
363
+ if (name !== "TimeoutError") return err;
364
+ const secs = Math.round((Date.now() - streamStartedAt) / 1e3);
365
+ return new Error(
366
+ `${provider} SSE stream timed out mid-body after ${secs}s of streaming \u2014 a timeout signal aborted the request while the body was still being read`
367
+ );
368
+ }
369
+
370
+ // src/api/openai-client.ts
371
+ var import_undici = __toESM(require_undici(), 1);
372
+ import { appendFileSync, mkdirSync } from "fs";
373
+ import { dirname, join } from "path";
374
+
375
+ // src/api/rate-limiter.ts
376
+ init_esm_shims();
377
+ var TokenBucket = class {
378
+ intervalMs;
379
+ burst;
380
+ nextSlotAt;
381
+ constructor(config) {
382
+ this.intervalMs = 1e3 / config.requestsPerSecond;
383
+ this.burst = config.burst ?? Math.max(1, Math.ceil(config.requestsPerSecond));
384
+ this.nextSlotAt = Date.now() - (this.burst - 1) * this.intervalMs;
385
+ }
386
+ async acquire(signal) {
387
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
388
+ const now = Date.now();
389
+ const earliest = now - (this.burst - 1) * this.intervalMs;
390
+ const slot = Math.max(earliest, this.nextSlotAt);
391
+ this.nextSlotAt = slot + this.intervalMs;
392
+ const waitMs = Math.ceil(slot - now);
393
+ if (waitMs <= 0) return;
394
+ await abortableDelay(waitMs, signal);
395
+ }
396
+ };
397
+ var registry = /* @__PURE__ */ new Map();
398
+ function getProviderRateLimiter(providerKey, config) {
399
+ if (!config || !(config.requestsPerSecond > 0)) return void 0;
400
+ const fingerprint = `${config.requestsPerSecond}:${config.burst ?? ""}`;
401
+ const existing = registry.get(providerKey);
402
+ if (existing && existing.fingerprint === fingerprint) return existing.limiter;
403
+ const limiter = new TokenBucket(config);
404
+ registry.set(providerKey, { fingerprint, limiter });
405
+ return limiter;
406
+ }
407
+ async function acquireRateLimitSlot(providerKey, config, signal) {
408
+ const limiter = getProviderRateLimiter(providerKey, config);
409
+ if (limiter) await limiter.acquire(signal);
410
+ }
411
+
412
+ // src/api/openai-client.ts
413
+ function wireHash(s) {
414
+ let h = 5381;
415
+ for (let i = 0; i < s.length; i++) {
416
+ h = (h << 5) + h + s.charCodeAt(i) | 0;
417
+ }
418
+ return `${h}:${s.length}`;
419
+ }
420
+ function tryParseToolArguments(raw) {
421
+ if (raw.trim().length === 0) return {};
422
+ try {
423
+ const parsed = JSON.parse(raw);
424
+ return parsed && typeof parsed === "object" ? parsed : null;
425
+ } catch {
426
+ const repaired = repairInvalidJsonEscapes(raw);
427
+ if (repaired !== null) {
428
+ try {
429
+ const parsed = JSON.parse(repaired);
430
+ if (parsed && typeof parsed === "object") return parsed;
431
+ } catch {
432
+ }
433
+ }
434
+ return salvageFirstJsonObject(repaired ?? raw);
435
+ }
436
+ }
437
+ function salvageFirstJsonObject(raw) {
438
+ const start = raw.indexOf("{");
439
+ if (start === -1) return null;
440
+ let depth = 0;
441
+ let inStr = false;
442
+ let escaped = false;
443
+ for (let i = start; i < raw.length; i++) {
444
+ const ch = raw[i];
445
+ if (inStr) {
446
+ if (escaped) escaped = false;
447
+ else if (ch === "\\") escaped = true;
448
+ else if (ch === '"') inStr = false;
449
+ continue;
450
+ }
451
+ if (ch === '"') inStr = true;
452
+ else if (ch === "{") depth++;
453
+ else if (ch === "}") {
454
+ depth--;
455
+ if (depth === 0) {
456
+ try {
457
+ const obj = JSON.parse(raw.slice(start, i + 1));
458
+ return obj && typeof obj === "object" ? obj : null;
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+ var FIRST_BYTE_TIMEOUT_MS = 45e3;
468
+ var REASONING_FIRST_BYTE_TIMEOUT_MS = 9e4;
469
+ var READ_TIMEOUT_MS = 12e4;
470
+ var REASONING_READ_TIMEOUT_MS = 18e4;
471
+ var SLOW_FIRST_BYTE_TIMEOUT_MS = 18e4;
472
+ var SLOW_READ_TIMEOUT_MS = 3e5;
473
+ var FIRST_BYTE_PER_100K_MS = 6e4;
474
+ var FIRST_BYTE_MAX_MS = 42e4;
475
+ var GLM_READ_TIMEOUT_MS = 72e4;
476
+ var SLOW_THINKING_PROVIDERS = /* @__PURE__ */ new Set(["glm", "mimo", "deepseek", "codex", "minimax"]);
477
+ var SLOW_THINKING_HOST_HINTS = ["deepseek", "bigmodel.cn", "xiaomimimo", "minimax", "chatgpt.com"];
478
+ function isSlowThinkingProvider(input) {
479
+ if (input.slowThinking !== void 0) return input.slowThinking;
480
+ if (SLOW_THINKING_PROVIDERS.has(input.providerName ?? "")) return true;
481
+ if (input.baseUrl) {
482
+ try {
483
+ const host = new URL(input.baseUrl).hostname.toLowerCase();
484
+ return SLOW_THINKING_HOST_HINTS.some((hint) => host.includes(hint));
485
+ } catch {
486
+ }
487
+ }
488
+ return false;
489
+ }
490
+ var TOOL_STREAM_LOG_MAX_LINES = 2e3;
491
+ function redactSensitiveArgs(fragment) {
492
+ return fragment.replace(
493
+ /("(?:[^"\\]*(?:password|passwd|secret|token|api[_-]?key|apikey|authorization|credential|private[_-]?key|access[_-]?key)[^"\\]*)"\s*:\s*")((?:[^"\\]|\\.)*)("?)/gi,
494
+ "$1[REDACTED]$3"
495
+ );
496
+ }
497
+ var HARD_CAP_PROGRESS_WINDOW_MS = 6e4;
498
+ var HARD_CAP_EXTENSION_SLICE_MS = 6e4;
499
+ var GLM_HARD_CAP_PROGRESS_WINDOW_MS = 12e4;
500
+ function computeFirstByteTimeoutMs(input) {
501
+ const per100kMs = input.per100kMs ?? FIRST_BYTE_PER_100K_MS;
502
+ const capMs = input.capMs ?? FIRST_BYTE_MAX_MS;
503
+ const buckets = Math.floor(Math.max(0, input.estInputTokens) / 1e5);
504
+ const scaled = input.baseMs + buckets * per100kMs;
505
+ return Math.min(scaled, capMs);
506
+ }
507
+ function decideStreamHardCap(input, progressWindowMs = HARD_CAP_PROGRESS_WINDOW_MS) {
508
+ const absoluteMaxMs = input.baseStreamMs * 3;
509
+ const elapsed = input.now - input.startedAt;
510
+ if (elapsed >= absoluteMaxMs) return { kind: "abort" };
511
+ if (elapsed >= input.baseStreamMs) {
512
+ if (input.now - input.lastDataEventAt > progressWindowMs) return { kind: "abort" };
513
+ return { kind: "rearm", rearmMs: Math.min(HARD_CAP_EXTENSION_SLICE_MS, absoluteMaxMs - elapsed), extended: true };
514
+ }
515
+ return { kind: "rearm", rearmMs: input.baseStreamMs - elapsed, extended: false };
516
+ }
517
+ var OpenAIClient = class {
518
+ constructor(config) {
519
+ this.config = config;
520
+ this.systemSuffix = Boolean(config.preservedThinkingProtocol) && config.thinking === "enabled" ? "\n\n\u8BF7\u5728\u5185\u90E8\u601D\u8003\u94FE\u4E2D\u4F7F\u7528\u4E2D\u6587\u8FDB\u884C\u63A8\u7406\u3002\u4E0D\u8981\u5728\u56DE\u590D\u4E2D\u8F93\u51FA\u4F60\u7684\u63A8\u7406\u8FC7\u7A0B\uFF0C\u53EA\u8F93\u51FA\u6700\u7EC8\u7B54\u6848\u6216\u5DE5\u5177\u8C03\u7528\u3002" : "";
521
+ this._sanitizedCount = 0;
522
+ this.proxyDispatcher = config.proxy ? new import_undici.ProxyAgent(config.proxy) : void 0;
523
+ }
524
+ config;
525
+ toolCallBuffer = /* @__PURE__ */ new Map();
526
+ toolCallHintFired = /* @__PURE__ */ new Set();
527
+ /** Resolved raw-SSE dump path (undefined = unresolved, null = disabled/failed). */
528
+ rawSsePath = void 0;
529
+ pendingStopReason = null;
530
+ /** Messages from the current stream request — used for usage calibration. */
531
+ lastRequestMessages = [];
532
+ /** Accumulated text for DeepSeek tool-JSON-in-content fallback (网#1). */
533
+ _textAccum = "";
534
+ /** Whether any content delta has started this stream (channel monotonicity:
535
+ * reasoning_content arriving after content is a protocol anomaly, reclassified as text). */
536
+ contentStarted = false;
537
+ /** Stable suffix appended to system message for Chinese thinking (computed once, cache-safe). */
538
+ systemSuffix;
539
+ /**
540
+ * Resolved path for the lightweight tool-stream event log (always-on, best-effort).
541
+ * undefined = unresolved, null = disabled/failed. Appends one JSON line per
542
+ * notable streaming event (pollution-risk drops, final-flush empties) so the
543
+ * cross-tool argument corruption class of bug leaves a trace WITHOUT requiring
544
+ * RIVET_DEBUG. Capped at TOOL_STREAM_LOG_MAX_LINES per process to bound disk.
545
+ */
546
+ toolStreamLogPath = void 0;
547
+ toolStreamLogLines = 0;
548
+ /** Wire-level prefix probe: per-message signatures of the previous main-turn
549
+ * request's FINAL bytes (post reasoning-strip / sanitize / system-suffix).
550
+ * Only updated for requests with `prefixProbe: true` so side-path calls
551
+ * through this client don't poison the baseline. */
552
+ prevWireSignatures = null;
553
+ /** 上一次主轮请求的 tools 数组指纹(同序 stable-json——上线字节保数组序)。
554
+ * 消息级探针对工具定义变化隐形(2026-09-06 主会话 toolsUpdated 碎裂事件
555
+ * 双探针皆净即此因),单独一个维度。 */
556
+ prevWireToolsSig = null;
557
+ /** Latest wire divergence (consume-once via consumeWireDivergence). */
558
+ lastWireDivergence = null;
559
+ /** undici ProxyAgent for config.proxy (undefined = no per-provider proxy). */
560
+ proxyDispatcher;
561
+ /** 慢思考端点判定(名称/URL/显式配置三级,见 isSlowThinkingProvider)。 */
562
+ get isSlowThinking() {
563
+ return isSlowThinkingProvider({
564
+ providerName: this.config.providerName,
565
+ baseUrl: this.config.baseUrl,
566
+ slowThinking: this.config.slowThinking
567
+ });
568
+ }
569
+ // ── Incremental sanitize ─────────────────────────────────────
570
+ // Historical messages are already sanitized at entry points
571
+ // (addUserMessage/addAssistantBlocks/addToolResults). Re-sanitizing
572
+ // the full message array every turn is O(history) overhead that grows
573
+ // linearly with conversation length — particularly wasteful at 1M
574
+ // context windows. We track the sanitized count and only apply the
575
+ // safety-net sanitize to newly appended messages.
576
+ _sanitizedCount;
577
+ setReasoningEffort(effort) {
578
+ this.config = { ...this.config, reasoningEffort: effort };
579
+ }
580
+ setThinking(mode) {
581
+ this.config = { ...this.config, thinking: mode };
582
+ }
583
+ async stream(request, callbacks, signal) {
584
+ this.lastRequestMessages = request.messages;
585
+ const isPreservedThinking = this.config.thinking === "enabled" && Boolean(this.config.preservedThinkingProtocol);
586
+ const messages = request.messages.map((m) => {
587
+ if (m.role !== "assistant") return m;
588
+ const hasToolCalls = Array.isArray(m.tool_calls) && m.tool_calls.length > 0;
589
+ if (isPreservedThinking && hasToolCalls && !("reasoning_content" in m)) {
590
+ return { ...m, reasoning_content: "" };
591
+ }
592
+ if (!("reasoning_content" in m)) return m;
593
+ if (isPreservedThinking && hasToolCalls) {
594
+ const transform = proRegistry.getWireTransform(this.config.providerName ?? "");
595
+ if (transform) return transform(m, this.config.model, this.config.wireContext);
596
+ return m;
597
+ }
598
+ const { reasoning_content: _, ...rest } = m;
599
+ if (!("content" in rest) && !hasToolCalls) {
600
+ rest.content = "";
601
+ }
602
+ return rest;
603
+ }).map(normalizeOaiMessage);
604
+ const body = {
605
+ // 空 model 回退到 client 绑定值——侧路调用(essence-gate / vision bridge)
606
+ // 传 model: '' 期望"client already binds the model";不回退会被 provider 400。
607
+ model: request.model || this.config.model,
608
+ messages,
609
+ stream: true
610
+ };
611
+ if (this.config.useMaxCompletionTokens) {
612
+ body.max_completion_tokens = request.max_tokens ?? this.config.maxTokens;
613
+ } else {
614
+ body.max_tokens = request.max_tokens ?? this.config.maxTokens;
615
+ }
616
+ if (!this.config.unsupported?.includes("stream_options")) {
617
+ body.stream_options = { include_usage: true };
618
+ }
619
+ if (request.tools && request.tools.length > 0) {
620
+ body.tools = request.tools;
621
+ if (request.tool_choice) body.tool_choice = request.tool_choice;
622
+ }
623
+ if (request.response_format) {
624
+ body.response_format = request.response_format;
625
+ } else if (this.config.jsonMode) {
626
+ body.response_format = { type: "json_object" };
627
+ }
628
+ if (request.temperature !== void 0) {
629
+ body.temperature = request.temperature;
630
+ } else if (this.config.temperature !== void 0 && this.config.thinking !== "enabled") {
631
+ body.temperature = this.config.temperature;
632
+ }
633
+ if (this.config.reasoningSplit) {
634
+ body.reasoning_split = true;
635
+ }
636
+ const blockType = this.config.thinkingBlockType ?? "none";
637
+ if (this.config.thinking === "enabled") {
638
+ if (blockType !== "none") {
639
+ body.thinking = { type: blockType };
640
+ if (this.config.thinkingBudgetField === "budget_tokens" && this.config.reasoningEffort) {
641
+ const budgetMap = {
642
+ max: this.config.maxTokens,
643
+ high: Math.floor(this.config.maxTokens * 0.6),
644
+ medium: Math.floor(this.config.maxTokens * 0.3),
645
+ low: 8192,
646
+ off: 0
647
+ };
648
+ const budget = budgetMap[this.config.reasoningEffort ?? "high"] ?? Math.floor(this.config.maxTokens * 0.6);
649
+ body.thinking["budget_tokens"] = budget;
650
+ }
651
+ if (this.config.effortFormat === "reasoning_effort" && this.config.reasoningEffort && this.config.reasoningEffort !== "off") {
652
+ body.reasoning_effort = this.config.reasoningEffort;
653
+ }
654
+ } else if (this.config.effortFormat !== "none") {
655
+ body.reasoning_effort = this.config.reasoningEffort ?? "medium";
656
+ }
657
+ }
658
+ if (request.reasoning_effort && this.config.effortFormat !== "none") {
659
+ body.reasoning_effort = request.reasoning_effort;
660
+ }
661
+ if (this.config.effortCap && typeof body.reasoning_effort === "string") {
662
+ const capped = this.config.effortCap[body.reasoning_effort];
663
+ if (capped) body.reasoning_effort = capped;
664
+ }
665
+ if (this.systemSuffix) {
666
+ const wireMessages = body.messages;
667
+ const sysIdx = wireMessages.findIndex((m) => m.role === "system");
668
+ const sysMsg = sysIdx >= 0 ? wireMessages[sysIdx] : void 0;
669
+ if (sysMsg && typeof sysMsg.content === "string") {
670
+ wireMessages[sysIdx] = { ...sysMsg, content: sysMsg.content + this.systemSuffix };
671
+ }
672
+ }
673
+ const msgArray = body.messages;
674
+ if (msgArray.length <= this._sanitizedCount) {
675
+ this._sanitizedCount = 0;
676
+ }
677
+ const newMessages = msgArray.slice(this._sanitizedCount);
678
+ if (newMessages.length > 0) {
679
+ const sanitizedNew = sanitizeMessageContent(newMessages);
680
+ for (let i = 0; i < sanitizedNew.length; i++) {
681
+ msgArray[this._sanitizedCount + i] = sanitizedNew[i];
682
+ }
683
+ this._sanitizedCount = msgArray.length;
684
+ }
685
+ if (request.prefixProbe) this.recordWireDivergence(msgArray, body.tools);
686
+ await this.sendStream(body, callbacks, signal);
687
+ }
688
+ /** Compare this request's final wire bytes with the previous main-turn
689
+ * request's; record the first diverged message. Pure appends record nothing.
690
+ * tools 数组单独成维——它不进 messages,消息级对比对其隐形。 */
691
+ recordWireDivergence(messages, tools) {
692
+ const sigs = messages.map((m) => {
693
+ const s = JSON.stringify(m);
694
+ return { sig: wireHash(s), len: s.length, role: String(m.role ?? "?") };
695
+ });
696
+ const toolsSig = tools && tools.length > 0 ? wireHash(stableStringify(tools)) : null;
697
+ const prev = this.prevWireSignatures;
698
+ const prevToolsSig = this.prevWireToolsSig;
699
+ this.prevWireSignatures = sigs;
700
+ this.prevWireToolsSig = toolsSig;
701
+ if (!prev) return;
702
+ if (prevToolsSig !== toolsSig) {
703
+ this.lastWireDivergence = {
704
+ idx: -1,
705
+ role: "tools",
706
+ kind: "tools_changed",
707
+ prevCount: prev.length,
708
+ newCount: sigs.length,
709
+ approxCharPos: 0
710
+ };
711
+ return;
712
+ }
713
+ const shared = Math.min(prev.length, sigs.length);
714
+ let divergedIdx = -1;
715
+ for (let i = 0; i < shared; i++) {
716
+ if (prev[i].sig !== sigs[i].sig) {
717
+ divergedIdx = i;
718
+ break;
719
+ }
720
+ }
721
+ if (divergedIdx === -1) {
722
+ if (sigs.length >= prev.length) return;
723
+ divergedIdx = sigs.length;
724
+ }
725
+ let approxCharPos = 0;
726
+ for (let i = 0; i < divergedIdx; i++) approxCharPos += sigs[i]?.len ?? prev[i].len;
727
+ this.lastWireDivergence = {
728
+ idx: divergedIdx,
729
+ role: sigs[divergedIdx]?.role ?? "removed",
730
+ kind: divergedIdx >= sigs.length ? "message_removed" : "message_changed",
731
+ prevCount: prev.length,
732
+ newCount: sigs.length,
733
+ approxCharPos
734
+ };
735
+ }
736
+ /** Consume-once accessor for the latest wire-level prefix divergence. */
737
+ consumeWireDivergence() {
738
+ const d = this.lastWireDivergence;
739
+ this.lastWireDivergence = null;
740
+ return d;
741
+ }
742
+ /** Shared inner retry+fetch+SSE loop used by both stream and streamOai. */
743
+ async sendStream(body, callbacks, signal) {
744
+ const reasoningRef = { content: "" };
745
+ const isThinking = this.config.thinking === "enabled";
746
+ let stripRequested = false;
747
+ let imagesStripped = false;
748
+ const estInputTokens = estimateOaiTokens(body.messages ?? []);
749
+ const derivedFirstByteBaseMs = isThinking ? this.isSlowThinking ? SLOW_FIRST_BYTE_TIMEOUT_MS : REASONING_FIRST_BYTE_TIMEOUT_MS : FIRST_BYTE_TIMEOUT_MS;
750
+ const firstByteMs = computeFirstByteTimeoutMs({
751
+ baseMs: this.config.firstByteTimeoutMs ?? derivedFirstByteBaseMs,
752
+ estInputTokens
753
+ });
754
+ await withStructuredRetry(async () => {
755
+ this.toolCallBuffer.clear();
756
+ this.toolCallHintFired.clear();
757
+ this.pendingStopReason = null;
758
+ this._textAccum = "";
759
+ this.contentStarted = false;
760
+ let wireMessages = body.messages;
761
+ if (stripRequested && !imagesStripped) {
762
+ const stripped = stripOaiImageParts(wireMessages);
763
+ if (stripped.removedCount > 0) {
764
+ wireMessages = stripped.messages;
765
+ imagesStripped = true;
766
+ callbacks.onImageStripped?.({ removedCount: stripped.removedCount });
767
+ }
768
+ }
769
+ let effectiveBody = body;
770
+ const isGlm = this.config.providerName === "glm";
771
+ if (isThinking && reasoningRef.content && !isGlm) {
772
+ const msgs = [...wireMessages, {
773
+ role: "assistant",
774
+ content: "",
775
+ reasoning_content: reasoningRef.content
776
+ }];
777
+ effectiveBody = { ...body, messages: msgs };
778
+ } else if (wireMessages !== body.messages) {
779
+ effectiveBody = { ...body, messages: wireMessages };
780
+ }
781
+ const authHeaders = this.config.auth ? await this.config.auth.getHeaders() : { "Authorization": `Bearer ${this.config.apiKey}` };
782
+ const fetchTimeout = firstByteMs;
783
+ const lifecycle = new AbortController();
784
+ if (signal) {
785
+ if (signal.aborted) lifecycle.abort();
786
+ else signal.addEventListener("abort", () => lifecycle.abort(), { once: true });
787
+ }
788
+ await acquireRateLimitSlot(this.config.providerName ?? this.config.baseUrl, this.config.retry?.rateLimit, lifecycle.signal);
789
+ const response = await fetchWithTimeout(`${normalizeBaseUrl(this.config.baseUrl)}/chat/completions`, {
790
+ method: "POST",
791
+ headers: {
792
+ "Content-Type": "application/json",
793
+ "Connection": "keep-alive",
794
+ ...this.config.userAgent ? { "User-Agent": this.config.userAgent } : {},
795
+ ...authHeaders,
796
+ ...this.config.sessionId ? { [this.config.sessionHeader ?? "X-Request-Session"]: this.config.sessionId } : {}
797
+ },
798
+ body: JSON.stringify(effectiveBody),
799
+ signal: lifecycle.signal
800
+ }, fetchTimeout, this.proxyDispatcher);
801
+ if (!response.ok) {
802
+ const errorBody = await response.text().catch(() => "");
803
+ const err = Object.assign(
804
+ new Error(parseOpenAIError(response.status, errorBody, { providerName: this.config.providerName, baseUrl: this.config.baseUrl, apiKeyEnv: this.config.apiKeyEnv })),
805
+ {
806
+ status: response.status,
807
+ // 413 的两种成因(图片过重 / 上下文超限)在 wire 层同形,分类器无法
808
+ // 自行区分——只有这里知道刚发出去的体里有没有图。标在错误上让分类器
809
+ // 分流:无图 = 纯上下文超限,重发同一个体必然再 413,不该重试。
810
+ ...response.status === 413 ? { payloadHadImages: oaiMessagesHaveImageParts(effectiveBody.messages ?? []) } : {}
811
+ }
812
+ );
813
+ const retryAfter = response.headers.get("retry-after");
814
+ if (retryAfter) {
815
+ const retryAfterMs = parseRetryAfterMs(retryAfter);
816
+ if (retryAfterMs !== void 0) {
817
+ ;
818
+ err.retryAfterMs = retryAfterMs;
819
+ }
820
+ }
821
+ throw err;
822
+ }
823
+ const contentType = response.headers.get("content-type") ?? "";
824
+ if (contentType && !contentType.includes("event-stream") && !contentType.includes("octet-stream")) {
825
+ const snippet = await response.text().catch(() => "");
826
+ throw Object.assign(
827
+ new Error(
828
+ `\u7AEF\u70B9\u8FD4\u56DE 200 \u4F46 content-type \u662F ${contentType}\uFF08\u4E0D\u662F SSE \u6D41\uFF09\u2014\u2014\u7AEF\u70B9\u53EF\u80FD\u4E0D\u652F\u6301\u6D41\u5F0F\uFF0C\u6216\u8DEF\u5F84\u9519\u8BEF\uFF08baseUrl \u662F\u5426\u7F3A /v1\uFF1F\uFF09\u3002baseUrl=${this.config.baseUrl}\u3002\u54CD\u5E94\u7247\u6BB5\uFF1A${snippet.slice(0, 200)}`
829
+ ),
830
+ { status: 200, nonSse: true }
831
+ );
832
+ }
833
+ const reader = response.body?.getReader();
834
+ if (!reader) throw new Error("Response body is not readable");
835
+ await this.parseStreamFromReader(reader, callbacks, signal, reasoningRef, lifecycle, firstByteMs);
836
+ }, signal, {
837
+ maxTotalDurationMs: this.config.retry?.maxTotalDurationMs ?? (this.config.providerName === "glm" ? 20 * 6e4 : 10 * 6e4),
838
+ // Thinking retries are normally throttled to 1 because re-reasoning is costly.
839
+ // 例外:slow-thinking providers 在被中止的那次尝试里已把整个 prompt 灌进服务端
840
+ // 前缀缓存,重试命中近 100% 缓存(实测 deepseek 99.4% hit、~12s 完成),代价极低。
841
+ // 给它们 2 次重试,让「单次服务端 thinking 卡死」甚至「连续两次卡死」都能自愈而
842
+ // 不冒泡成错误。maxTotalDurationMs 仍是总时长兜底,防 runaway。
843
+ // An explicit provider maxRetries overrides the built-in thinking budget (0 disables retries).
844
+ maxTotalRetries: this.config.maxRetries ?? (isThinking ? this.isSlowThinking ? 2 : 1 : void 0),
845
+ policy: this.config.retry,
846
+ onRetry: (info) => {
847
+ if (info.classified.category === "rate_limit") {
848
+ callbacks.onRateLimit?.(info.classified.retryDelayMs);
849
+ }
850
+ if (info.classified.category === "image_strip") {
851
+ stripRequested = true;
852
+ }
853
+ }
854
+ });
855
+ }
856
+ /** Parse SSE stream from a reader — exposed for testing */
857
+ /** Parse SSE stream from a reader — exposed for testing */
858
+ async parseStreamFromReader(reader, callbacks, signal, reasoningRef, lifecycle, firstByteTimeoutMs) {
859
+ const decoder = new TextDecoder();
860
+ let buffer = "";
861
+ let streamTimedOut = false;
862
+ let idleTimer = null;
863
+ let receivedFirstChunk = false;
864
+ let lastIdleTimeoutMs = 0;
865
+ let lastFiredAsThinkingStall = false;
866
+ let receivedThinking = false;
867
+ let reasoningAccum = "";
868
+ let textReceived = false;
869
+ let promotionFired = false;
870
+ let toolProgressReceived = false;
871
+ const repetitionGuard = this.config.providerName === "deepseek" ? new ReasoningRepetitionGuard() : void 0;
872
+ const processPayload = (payload) => {
873
+ let parsed;
874
+ try {
875
+ parsed = JSON.parse(payload);
876
+ } catch {
877
+ return;
878
+ }
879
+ if (!parsed || typeof parsed !== "object") return;
880
+ const delta = parsed?.choices?.[0]?.delta;
881
+ if (parsed.choices?.[0] && !delta) return;
882
+ if (delta?.tool_calls?.length) toolProgressReceived = true;
883
+ if (delta?.reasoning_content && !textReceived) {
884
+ reasoningAccum += delta.reasoning_content;
885
+ receivedThinking = true;
886
+ if (reasoningRef) reasoningRef.content = reasoningAccum;
887
+ if (!delta.content && !toolProgressReceived && this.toolCallBuffer.size === 0) {
888
+ repetitionGuard?.push(delta.reasoning_content);
889
+ }
890
+ }
891
+ this.processDelta(parsed, callbacks);
892
+ if (delta?.content) textReceived = true;
893
+ };
894
+ const timeoutController = new AbortController();
895
+ const isGlm = this.config.providerName === "glm";
896
+ const explicitRequestTimeoutMs = this.config.requestTimeoutMs;
897
+ const baseStreamMs = explicitRequestTimeoutMs ?? (isGlm ? 20 * 6e4 : 10 * 6e4);
898
+ const streamStartedAt = Date.now();
899
+ let lastDataEventAt = streamStartedAt;
900
+ let hardCapExtended = false;
901
+ const progressWindowMs = isGlm ? GLM_HARD_CAP_PROGRESS_WINDOW_MS : HARD_CAP_PROGRESS_WINDOW_MS;
902
+ const checkHardCap = () => {
903
+ const action = decideStreamHardCap({
904
+ now: Date.now(),
905
+ startedAt: streamStartedAt,
906
+ lastDataEventAt,
907
+ baseStreamMs
908
+ }, progressWindowMs);
909
+ if (action.kind === "abort") {
910
+ timeoutController.abort();
911
+ return;
912
+ }
913
+ if (action.extended) hardCapExtended = true;
914
+ maxStreamTimer = setTimeout(checkHardCap, action.rearmMs);
915
+ };
916
+ let maxStreamTimer = explicitRequestTimeoutMs !== void 0 ? setTimeout(() => timeoutController.abort(), explicitRequestTimeoutMs) : setTimeout(checkHardCap, baseStreamMs);
917
+ const signalCleanup = signal ? wireAbortToReaderCancel(AbortSignal.any([signal, timeoutController.signal]), reader) : wireAbortToReaderCancel(timeoutController.signal, reader);
918
+ const resetIdleTimer = () => {
919
+ if (idleTimer) clearTimeout(idleTimer);
920
+ const isReasoning = this.config.thinking === "enabled";
921
+ const isSlowProvider = this.isSlowThinking;
922
+ const firstByteMs = firstByteTimeoutMs ?? (isSlowProvider ? SLOW_FIRST_BYTE_TIMEOUT_MS : isReasoning ? REASONING_FIRST_BYTE_TIMEOUT_MS : FIRST_BYTE_TIMEOUT_MS);
923
+ const isGlm2 = this.config.providerName === "glm";
924
+ const readMs = isGlm2 && isReasoning ? GLM_READ_TIMEOUT_MS : isSlowProvider ? SLOW_READ_TIMEOUT_MS : isReasoning ? REASONING_READ_TIMEOUT_MS : READ_TIMEOUT_MS;
925
+ const hasActionableBlock = this.toolCallBuffer.size > 0 || this._textAccum.length > 0;
926
+ const inThinkingOnly = receivedThinking && !textReceived && !hasActionableBlock;
927
+ const thinkingStallMs = inThinkingOnly ? Math.min(this.config.thinkingStallTimeoutMs ?? readMs, readMs) : null;
928
+ const timeout = receivedFirstChunk ? thinkingStallMs ?? readMs : firstByteMs;
929
+ lastIdleTimeoutMs = timeout;
930
+ lastFiredAsThinkingStall = thinkingStallMs !== null && thinkingStallMs < readMs;
931
+ idleTimer = setTimeout(() => {
932
+ streamTimedOut = true;
933
+ reader.cancel().catch(() => {
934
+ });
935
+ }, timeout);
936
+ };
937
+ try {
938
+ resetIdleTimer();
939
+ let streamDone = false;
940
+ const throwIfHardCapAborted = () => {
941
+ if (!timeoutController.signal.aborted) return;
942
+ const mins = Math.round((Date.now() - streamStartedAt) / 6e4);
943
+ throw new Error(explicitRequestTimeoutMs !== void 0 ? `OpenAI SSE stream request timeout (${Math.round(explicitRequestTimeoutMs / 1e3)}s, provider requestTimeoutMs) \u2014 total request duration exceeded configured limit` : hardCapExtended ? `OpenAI SSE stream hard timeout (~${mins}min, progress-extended) \u2014 stream exceeded maximum duration` : "OpenAI SSE stream hard timeout (10min) \u2014 stream stopped progressing");
944
+ };
945
+ while (!streamDone) {
946
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
947
+ throwIfHardCapAborted();
948
+ const { done, value } = await reader.read();
949
+ if (streamTimedOut) {
950
+ const secs = Math.round(lastIdleTimeoutMs / 1e3);
951
+ const msg = lastFiredAsThinkingStall ? `OpenAI SSE stream thinking stall timeout (${secs}s) \u2014 model stopped producing thinking tokens` : `OpenAI SSE stream idle timeout (${secs}s)`;
952
+ throw new Error(msg);
953
+ }
954
+ if (done) {
955
+ throwIfHardCapAborted();
956
+ break;
957
+ }
958
+ receivedFirstChunk = true;
959
+ buffer += decoder.decode(value, { stream: true });
960
+ const lines = buffer.split("\n");
961
+ buffer = lines.pop() ?? "";
962
+ let sawDataEvent = false;
963
+ for (const line of lines) {
964
+ const trimmed2 = line.trim();
965
+ if (!trimmed2 || !trimmed2.startsWith("data:")) continue;
966
+ const payload = trimmed2.slice(5).trimStart();
967
+ if (payload === "[DONE]") {
968
+ streamDone = true;
969
+ break;
970
+ }
971
+ sawDataEvent = true;
972
+ if (process.env.RIVET_DEBUG_RAW_SSE) this.dumpRawSse(payload);
973
+ processPayload(payload);
974
+ }
975
+ if (sawDataEvent) {
976
+ lastDataEventAt = Date.now();
977
+ resetIdleTimer();
978
+ }
979
+ }
980
+ const trimmed = buffer.trim();
981
+ if (trimmed.startsWith("data:")) {
982
+ const payload = trimmed.slice(5).trimStart();
983
+ if (payload !== "[DONE]") processPayload(payload);
984
+ }
985
+ this.flushToolCalls(callbacks, { final: true });
986
+ let textConsumedAsToolJson = false;
987
+ if (this.toolCallBuffer.size === 0 && this._textAccum && this.config.capabilities?.hasToolJsonInContentBug) {
988
+ textConsumedAsToolJson = this.tryParseToolJsonFromContent(this._textAccum, callbacks) > 0;
989
+ }
990
+ if (reasoningAccum) {
991
+ callbacks.onContentBlock?.({ type: "thinking", thinking: reasoningAccum });
992
+ }
993
+ if (!textReceived && reasoningAccum && this.config.providerName === "glm") {
994
+ callbacks.onTextDelta?.(reasoningAccum);
995
+ promotionFired = true;
996
+ }
997
+ const finalText = !textConsumedAsToolJson && this._textAccum ? this._textAccum : promotionFired ? reasoningAccum : "";
998
+ if (finalText) {
999
+ callbacks.onContentBlock?.({ type: "text", text: finalText });
1000
+ }
1001
+ this._textAccum = "";
1002
+ this.contentStarted = false;
1003
+ if (this.pendingStopReason) {
1004
+ callbacks.onStopReason?.(mapFinishReason(this.pendingStopReason), {});
1005
+ this.pendingStopReason = null;
1006
+ }
1007
+ } catch (err) {
1008
+ void reader.cancel().catch(() => {
1009
+ });
1010
+ callbacks.onStreamAttemptAborted?.({
1011
+ provider: this.config.providerName ?? "openai",
1012
+ receivedChars: reasoningAccum.length + this._textAccum.length,
1013
+ elapsedMs: Date.now() - streamStartedAt,
1014
+ errorName: err?.name ?? "Error",
1015
+ errorMessage: err?.message ?? String(err)
1016
+ });
1017
+ throw wrapBodyTimeoutError(err, "OpenAI", streamStartedAt);
1018
+ } finally {
1019
+ if (idleTimer) clearTimeout(idleTimer);
1020
+ if (maxStreamTimer) clearTimeout(maxStreamTimer);
1021
+ if (signalCleanup) signalCleanup();
1022
+ lifecycle?.abort();
1023
+ if (!textReceived && reasoningAccum && !promotionFired && this.config.providerName === "glm") {
1024
+ callbacks.onTextDelta?.(reasoningAccum);
1025
+ }
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Resolve which toolCallBuffer slot a streaming chunk belongs to.
1030
+ *
1031
+ * Returns the slot index, or null to DROP the chunk. Dropping is the
1032
+ * fail-safe choice when a continuation chunk carries no `index`/`id` and more
1033
+ * than one buffer is open — misgrafting it onto another tool's arguments
1034
+ * corrupts both (the parallel tool_call pollution bug). A dropped trailing
1035
+ * fragment at worst leaves one call's JSON incomplete, which the existing
1036
+ * final-flush + salvageFirstJsonObject path already handles.
1037
+ */
1038
+ resolveToolCallIndex(tc) {
1039
+ if (tc.index !== void 0) return tc.index;
1040
+ if (tc.id !== void 0) {
1041
+ for (const [idx, buf] of this.toolCallBuffer) {
1042
+ if (buf.id === tc.id) {
1043
+ this.logToolStreamEvent({
1044
+ phase: "reattach-by-id",
1045
+ openBuffers: this.toolCallBuffer.size,
1046
+ id: tc.id,
1047
+ name: buf.function.name,
1048
+ argsLen: tc.function?.arguments?.length
1049
+ });
1050
+ return idx;
1051
+ }
1052
+ }
1053
+ }
1054
+ if (this.toolCallBuffer.size === 1) {
1055
+ const idx = this.toolCallBuffer.keys().next().value;
1056
+ this.logToolStreamEvent({
1057
+ phase: "reattach-sole",
1058
+ openBuffers: 1,
1059
+ argsLen: tc.function?.arguments?.length
1060
+ });
1061
+ return idx;
1062
+ }
1063
+ if (this.toolCallBuffer.size === 0) return 0;
1064
+ this.logToolStreamEvent({
1065
+ phase: "drop-ambiguous",
1066
+ openBuffers: this.toolCallBuffer.size,
1067
+ argsLen: tc.function?.arguments?.length
1068
+ });
1069
+ return null;
1070
+ }
1071
+ /** Process a single SSE delta chunk — exposed for testing */
1072
+ processDelta(chunk, callbacks) {
1073
+ const choice = chunk.choices?.[0];
1074
+ if (chunk.usage && choice === void 0) {
1075
+ const usage = chunk.usage;
1076
+ const stopReason = this.pendingStopReason ?? "end_turn";
1077
+ this.pendingStopReason = null;
1078
+ const cacheRead = usage.prompt_cache_hit_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? 0;
1079
+ callbacks.onStopReason?.(mapFinishReason(stopReason), this.calibrateUsage({
1080
+ input_tokens: usage.prompt_tokens ?? 0,
1081
+ output_tokens: usage.completion_tokens ?? 0,
1082
+ cache_read_input_tokens: cacheRead,
1083
+ cache_creation_input_tokens: usage.prompt_cache_miss_tokens ?? 0,
1084
+ reasoning_tokens: usage.completion_tokens_details?.reasoning_tokens
1085
+ }));
1086
+ return;
1087
+ }
1088
+ if (!choice) return;
1089
+ const delta = choice.delta;
1090
+ if (delta.reasoning_content) {
1091
+ if (this.contentStarted) {
1092
+ callbacks.onTextDelta?.(delta.reasoning_content);
1093
+ this._textAccum += delta.reasoning_content;
1094
+ } else {
1095
+ callbacks.onThinkingDelta?.(delta.reasoning_content);
1096
+ }
1097
+ }
1098
+ if (delta.content) {
1099
+ this.contentStarted = true;
1100
+ callbacks.onTextDelta?.(delta.content);
1101
+ this._textAccum += delta.content;
1102
+ }
1103
+ if (delta.tool_calls) {
1104
+ for (const tc of delta.tool_calls) {
1105
+ callbacks.onToolCallDelta?.();
1106
+ const idx = this.resolveToolCallIndex(tc);
1107
+ if (idx === null) continue;
1108
+ const buf = this.toolCallBuffer.get(idx) ?? { function: { arguments: "" } };
1109
+ if (tc.id) buf.id = tc.id;
1110
+ if (tc.type) buf.type = tc.type;
1111
+ if (tc.function?.name) {
1112
+ buf.function.name = (buf.function.name ?? "") + tc.function.name;
1113
+ }
1114
+ if (tc.function?.arguments) {
1115
+ buf.function.arguments += tc.function.arguments;
1116
+ }
1117
+ this.toolCallBuffer.set(idx, buf);
1118
+ if (callbacks.onToolCallHint && buf.function.name && !this.toolCallHintFired.has(idx)) {
1119
+ try {
1120
+ const partial = JSON.parse(buf.function.arguments);
1121
+ this.toolCallHintFired.add(idx);
1122
+ callbacks.onToolCallHint(buf.function.name, partial);
1123
+ } catch {
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ if (choice.finish_reason) {
1129
+ this.flushToolCalls(callbacks);
1130
+ this.pendingStopReason = choice.finish_reason;
1131
+ }
1132
+ if (chunk.usage && this.pendingStopReason !== null) {
1133
+ const usage = chunk.usage;
1134
+ const stopReason = this.pendingStopReason;
1135
+ this.pendingStopReason = null;
1136
+ const cacheRead = usage.prompt_cache_hit_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? 0;
1137
+ callbacks.onStopReason?.(mapFinishReason(stopReason), this.calibrateUsage({
1138
+ input_tokens: usage.prompt_tokens ?? 0,
1139
+ output_tokens: usage.completion_tokens ?? 0,
1140
+ cache_read_input_tokens: cacheRead,
1141
+ cache_creation_input_tokens: usage.prompt_cache_miss_tokens ?? 0,
1142
+ reasoning_tokens: usage.completion_tokens_details?.reasoning_tokens
1143
+ }));
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Emit buffered tool_use blocks.
1148
+ *
1149
+ * Called twice per stream: once when `finish_reason` arrives (non-final) and
1150
+ * once at end-of-stream (`final: true`). On the non-final flush, entries whose
1151
+ * arguments are not yet valid JSON are LEFT in the buffer so trailing argument
1152
+ * deltas (GLM-5.2 sends them after finish_reason) can complete them before the
1153
+ * final flush. Only emitted entries are removed — the buffer is never cleared
1154
+ * wholesale, so a deferred entry survives to the final flush.
1155
+ */
1156
+ flushToolCalls(callbacks, opts = {}) {
1157
+ const final = opts.final ?? false;
1158
+ for (const [idx, buf] of this.toolCallBuffer) {
1159
+ if (!buf.id || !buf.function.name) {
1160
+ if (final) this.toolCallBuffer.delete(idx);
1161
+ continue;
1162
+ }
1163
+ const parsed = tryParseToolArguments(buf.function.arguments);
1164
+ if (parsed === null) {
1165
+ if (!final) {
1166
+ this.maybeTraceToolStream("defer", idx, buf);
1167
+ continue;
1168
+ }
1169
+ this.warnToolArgParseFailure(buf);
1170
+ this.logToolStreamEvent({
1171
+ phase: "final-flush-empty",
1172
+ openBuffers: this.toolCallBuffer.size,
1173
+ id: buf.id,
1174
+ name: buf.function.name,
1175
+ argsLen: buf.function.arguments.length
1176
+ });
1177
+ callbacks.onContentBlock?.({ type: "tool_use", id: buf.id, name: buf.function.name, input: {}, argsTruncated: true });
1178
+ this.toolCallBuffer.delete(idx);
1179
+ continue;
1180
+ }
1181
+ this.maybeTraceToolStream(final ? "emit-final" : "emit", idx, buf);
1182
+ callbacks.onContentBlock?.({ type: "tool_use", id: buf.id, name: buf.function.name, input: parsed });
1183
+ this.toolCallBuffer.delete(idx);
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Append one raw SSE `data:` payload to disk for offline analysis.
1188
+ * Gated by RIVET_DEBUG_RAW_SSE: `1` → `<cwd>/.rivet/raw-sse[-<sessionId>].jsonl`,
1189
+ * any other value is treated as an explicit target file path.
1190
+ */
1191
+ dumpRawSse(payload) {
1192
+ const env = process.env.RIVET_DEBUG_RAW_SSE;
1193
+ if (!env) return;
1194
+ if (this.rawSsePath === void 0) {
1195
+ const target = env === "1" ? join(process.cwd(), ".rivet", `raw-sse${this.config.sessionId ? `-${this.config.sessionId}` : ""}.jsonl`) : env;
1196
+ try {
1197
+ mkdirSync(dirname(target), { recursive: true });
1198
+ this.rawSsePath = target;
1199
+ } catch {
1200
+ this.rawSsePath = null;
1201
+ }
1202
+ }
1203
+ if (!this.rawSsePath) return;
1204
+ try {
1205
+ appendFileSync(this.rawSsePath, `${JSON.stringify({ t: Date.now(), model: this.config.model, payload })}
1206
+ `);
1207
+ } catch {
1208
+ }
1209
+ }
1210
+ /** Gated stream-level tool-call diagnostics (RIVET_DEBUG_TOOL_STREAM=1). */
1211
+ maybeTraceToolStream(phase, idx, buf) {
1212
+ if (process.env.RIVET_DEBUG_TOOL_STREAM !== "1") return;
1213
+ debugLog(
1214
+ `[tool-stream] phase=${phase} idx=${idx} id=${buf.id ?? "?"} name=${buf.function.name ?? "?"} argsLen=${buf.function.arguments.length} args=${JSON.stringify(redactSensitiveArgs(buf.function.arguments.slice(0, 200)))}`
1215
+ );
1216
+ }
1217
+ /**
1218
+ * Append one notable tool-stream event to the always-on session log. Unlike
1219
+ * maybeTraceToolStream (gated, stderr), this writes to disk by default so the
1220
+ * cross-tool argument-pollution class of bug leaves a trace without anyone
1221
+ * having to enable RIVET_DEBUG up front. Best-effort: any IO failure disables
1222
+ * further logging for this client (sets path to null), never throws.
1223
+ *
1224
+ * Security: events carry argsLen only, NEVER raw argument content — tool
1225
+ * arguments can embed secrets (passwords/tokens passed to Bash etc.) and an
1226
+ * always-on disk log must not capture them (CWE-532). Content previews exist
1227
+ * only in the RIVET_DEBUG_TOOL_STREAM stderr path, redacted.
1228
+ *
1229
+ * Capped at TOOL_STREAM_LOG_MAX_LINES per process — once hit, logging stops to
1230
+ * bound disk on long sessions. The cap is generous: these events are rare
1231
+ * (only fire on ambiguous continuation chunks / final-flush empties), so
1232
+ * hitting the cap itself signals a provider streaming pathology worth flagging.
1233
+ */
1234
+ logToolStreamEvent(event) {
1235
+ if (this.toolStreamLogPath === null) return;
1236
+ if (this.toolStreamLogLines >= TOOL_STREAM_LOG_MAX_LINES) return;
1237
+ if (this.toolStreamLogPath === void 0) {
1238
+ try {
1239
+ const file = join(
1240
+ process.cwd(),
1241
+ ".rivet",
1242
+ `tool-stream${this.config.sessionId ? `-${this.config.sessionId}` : ""}.jsonl`
1243
+ );
1244
+ mkdirSync(dirname(file), { recursive: true });
1245
+ this.toolStreamLogPath = file;
1246
+ } catch {
1247
+ this.toolStreamLogPath = null;
1248
+ return;
1249
+ }
1250
+ }
1251
+ try {
1252
+ appendFileSync(this.toolStreamLogPath, `${JSON.stringify({
1253
+ t: (/* @__PURE__ */ new Date()).toISOString(),
1254
+ model: this.config.model,
1255
+ provider: this.config.providerName ?? null,
1256
+ session: this.config.sessionId ?? null,
1257
+ ...event
1258
+ })}
1259
+ `);
1260
+ this.toolStreamLogLines++;
1261
+ } catch {
1262
+ this.toolStreamLogPath = null;
1263
+ }
1264
+ }
1265
+ /** Diagnostic log when a tool call's arguments never became valid JSON.
1266
+ *
1267
+ * Kept debug-only to avoid writing directly to stderr and corrupting the TUI
1268
+ * render (the failure is already surfaced to the model/user through the
1269
+ * `argsTruncated` tool_use block + tool_result error). Offline diagnosis is
1270
+ * still available via the tool-stream JSONL event log and RIVET_DEBUG.
1271
+ */
1272
+ warnToolArgParseFailure(buf) {
1273
+ const msg = `[tool-arg-parse-failure] id=${buf.id ?? "?"} name=${buf.function.name ?? "?"} argsLen=${buf.function.arguments.length} args=${JSON.stringify(redactSensitiveArgs(buf.function.arguments.slice(0, 300)))}`;
1274
+ debugLog(msg);
1275
+ }
1276
+ /**
1277
+ * Calibrate API-reported usage for providers with inflated prompt_tokens.
1278
+ * When usageCalibrationFactor=0 (GLM coding API), replace input_tokens with
1279
+ * a local chars/4 estimate and scale cache fields proportionally.
1280
+ */
1281
+ calibrateUsage(usage) {
1282
+ const factor = this.config.usageCalibrationFactor;
1283
+ if (factor === void 0 || factor >= 1) return usage;
1284
+ if (factor <= 0) {
1285
+ const estTokens = estimateRequestTokens(this.lastRequestMessages);
1286
+ const apiRatio = usage.input_tokens > 0 ? estTokens / usage.input_tokens : 1;
1287
+ return {
1288
+ ...usage,
1289
+ input_tokens: estTokens,
1290
+ cache_read_input_tokens: Math.round(usage.cache_read_input_tokens * apiRatio),
1291
+ cache_creation_input_tokens: Math.round(usage.cache_creation_input_tokens * apiRatio)
1292
+ };
1293
+ }
1294
+ return {
1295
+ ...usage,
1296
+ input_tokens: Math.round(usage.input_tokens * factor),
1297
+ cache_read_input_tokens: Math.round(usage.cache_read_input_tokens * factor),
1298
+ cache_creation_input_tokens: Math.round(usage.cache_creation_input_tokens * factor)
1299
+ };
1300
+ }
1301
+ /** 网#1: Parse tool JSON from accumulated text content (DeepSeek bug workaround).
1302
+ * Returns the number of tool_use blocks emitted so the caller knows whether
1303
+ * the text was consumed as tool calls (and must not also persist as text). */
1304
+ tryParseToolJsonFromContent(text, callbacks) {
1305
+ const trimmed = text.trim();
1306
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return 0;
1307
+ try {
1308
+ const parsed = JSON.parse(trimmed);
1309
+ const toolCalls = Array.isArray(parsed) ? parsed : [parsed];
1310
+ let emitted = 0;
1311
+ for (const tc of toolCalls) {
1312
+ if (typeof tc !== "object" || tc === null) continue;
1313
+ const obj = tc;
1314
+ if (typeof obj.name !== "string") continue;
1315
+ const input = typeof obj.arguments === "object" && obj.arguments !== null ? obj.arguments : typeof obj.arguments === "string" ? (() => {
1316
+ try {
1317
+ return JSON.parse(obj.arguments);
1318
+ } catch {
1319
+ return {};
1320
+ }
1321
+ })() : {};
1322
+ callbacks.onContentBlock?.({ type: "tool_use", id: `fallback_${obj.name}_${emitted}`, name: obj.name, input });
1323
+ emitted++;
1324
+ }
1325
+ return emitted;
1326
+ } catch {
1327
+ return 0;
1328
+ }
1329
+ }
1330
+ };
1331
+ function estimateRequestTokens(messages) {
1332
+ let chars = 0;
1333
+ for (const msg of messages) {
1334
+ if (typeof msg.content === "string") chars += msg.content.length;
1335
+ else if (Array.isArray(msg.content)) {
1336
+ for (const part of msg.content) {
1337
+ if (typeof part === "object" && part !== null && "text" in part) {
1338
+ chars += String(part.text).length;
1339
+ }
1340
+ }
1341
+ }
1342
+ if (msg.role === "assistant") {
1343
+ const m = msg;
1344
+ if (typeof m.reasoning_content === "string") chars += m.reasoning_content.length;
1345
+ if (Array.isArray(m.tool_calls)) chars += JSON.stringify(m.tool_calls).length;
1346
+ }
1347
+ }
1348
+ return Math.ceil(chars / 4);
1349
+ }
1350
+ function mapFinishReason(reason) {
1351
+ switch (reason) {
1352
+ case "stop":
1353
+ return "end_turn";
1354
+ case "tool_calls":
1355
+ return "tool_use";
1356
+ case "length":
1357
+ return "max_tokens";
1358
+ case "insufficient_system_resource":
1359
+ return "end_turn";
1360
+ // DeepSeek-specific
1361
+ default:
1362
+ return "end_turn";
1363
+ }
1364
+ }
1365
+ function apiErrorHint(code, message, provider) {
1366
+ const probe = `${code} ${typeof message === "string" ? message : ""}`;
1367
+ if (!/insufficient[ _-]?(balance|quota)|余额不足|额度不足/i.test(probe)) return "";
1368
+ const where = `${provider?.providerName ?? ""} ${provider?.baseUrl ?? ""}`.toLowerCase();
1369
+ const BILLING = [
1370
+ [/deepseek/, "DeepSeek", "https://platform.deepseek.com/top_up"],
1371
+ [/siliconflow|硅基/, "SiliconFlow", "https://cloud.siliconflow.cn"],
1372
+ [/bigmodel|zhipu|智谱|glm/, "\u667A\u8C31 GLM", "https://www.bigmodel.cn"],
1373
+ [/minimax/, "MiniMax", "https://platform.minimaxi.com"],
1374
+ [/moonshot|kimi/, "Kimi", "https://platform.moonshot.cn"]
1375
+ ];
1376
+ for (const [re, name, url] of BILLING) {
1377
+ if (re.test(where)) {
1378
+ return `
1379
+ \u63D0\u793A\uFF1A${name} \u8D26\u6237\u4F59\u989D\u4E0D\u8DB3\uFF0C\u5145\u503C\u540E\u91CD\u8BD5\uFF1A${url} \u2014\u2014 \u6216\u7528 /model \u4E34\u65F6\u5207\u6362\u5230\u5176\u4ED6 provider\u3002`;
1380
+ }
1381
+ }
1382
+ return "\n\u63D0\u793A\uFF1A\u5F53\u524D provider \u8D26\u6237\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 provider\u3002";
1383
+ }
1384
+ function statusHint(status, provider) {
1385
+ if (status === 401 || status === 403) {
1386
+ const envPart = provider?.apiKeyEnv ? `\u2014\u2014\u8BF7\u68C0\u67E5\u73AF\u5883\u53D8\u91CF ${provider.apiKeyEnv} \u662F\u5426\u5DF2\u5BFC\u51FA\u4E14\u672A\u8FC7\u671F` : "\u2014\u2014\u8BF7\u68C0\u67E5 API key \u662F\u5426\u6B63\u786E";
1387
+ return `
1388
+ \u63D0\u793A\uFF1A\u9274\u6743\u5931\u8D25\uFF08HTTP ${status}\uFF09${envPart}\uFF0C\u6216\u7528 /connect \u91CD\u65B0\u914D\u7F6E\u3002`;
1389
+ }
1390
+ if (status === 404) {
1391
+ return "\n\u63D0\u793A\uFF1A404 \u901A\u5E38\u662F\u6A21\u578B id \u62FC\u9519\u6216\u7AEF\u70B9\u8DEF\u5F84\u4E0D\u5BF9\u2014\u2014\u8FD0\u884C `rivet provider models <provider>` \u6838\u5BF9\u7AEF\u70B9\u5B9E\u9645\u63D0\u4F9B\u7684\u6A21\u578B id\u3002";
1392
+ }
1393
+ return "";
1394
+ }
1395
+ function parseOpenAIError(status, body, provider) {
1396
+ try {
1397
+ const parsed = JSON.parse(body);
1398
+ const code = parsed.error?.code ?? parsed.error?.type ?? `HTTP ${status}`;
1399
+ const message = parsed.error?.message ?? body;
1400
+ return `OpenAI API error (${code}): ${message}${apiErrorHint(String(code), String(message), provider)}${statusHint(status, provider)}`;
1401
+ } catch {
1402
+ return `OpenAI API error (HTTP ${status}): ${body}${statusHint(status, provider)}`;
1403
+ }
1404
+ }
1405
+
1406
+ // src/api/codex-client.ts
1407
+ init_esm_shims();
1408
+ var CODEX_USER_AGENT = "codex_cli_rs/0.118.0 (Mac OS 26.3.1; arm64) iTerm.app/3.6.9";
1409
+ var CODEX_ORIGINATOR = "codex_cli_rs";
1410
+ function extractReasoningTokens(usage) {
1411
+ const details = usage.output_tokens_details;
1412
+ const reasoning = details?.reasoning_tokens;
1413
+ return typeof reasoning === "number" ? reasoning : void 0;
1414
+ }
1415
+ var CodexClient = class {
1416
+ constructor(config) {
1417
+ this.config = config;
1418
+ }
1419
+ config;
1420
+ setReasoningEffort(_effort) {
1421
+ }
1422
+ setThinking(_mode) {
1423
+ }
1424
+ async stream(request, callbacks, signal) {
1425
+ const body = this.buildRequestBody(request);
1426
+ await withStructuredRetry(async () => {
1427
+ const authHeaders = this.config.auth ? await this.config.auth.getHeaders() : {};
1428
+ const url = `${this.config.baseUrl.replace(/\/+$/, "")}/responses`;
1429
+ const lifecycle = new AbortController();
1430
+ if (signal) {
1431
+ if (signal.aborted) lifecycle.abort();
1432
+ else signal.addEventListener("abort", () => lifecycle.abort(), { once: true });
1433
+ }
1434
+ await acquireRateLimitSlot(this.config.baseUrl, this.config.retry?.rateLimit, lifecycle.signal);
1435
+ const response = await fetchWithTimeout(url, {
1436
+ method: "POST",
1437
+ headers: {
1438
+ "Content-Type": "application/json",
1439
+ "User-Agent": CODEX_USER_AGENT,
1440
+ "Originator": CODEX_ORIGINATOR,
1441
+ "Accept": "text/event-stream",
1442
+ "Connection": "Keep-Alive",
1443
+ ...authHeaders
1444
+ },
1445
+ body: JSON.stringify(body),
1446
+ signal: lifecycle.signal
1447
+ }, 18e4);
1448
+ if (!response.ok) {
1449
+ const errorBody = await response.text().catch(() => "");
1450
+ const err = Object.assign(
1451
+ new Error(`Codex API error (${response.status}): ${errorBody}`),
1452
+ { status: response.status }
1453
+ );
1454
+ const retryAfter = response.headers.get("retry-after");
1455
+ if (retryAfter) {
1456
+ const retryAfterMs = parseRetryAfterMs(retryAfter);
1457
+ if (retryAfterMs !== void 0) {
1458
+ ;
1459
+ err.retryAfterMs = retryAfterMs;
1460
+ }
1461
+ }
1462
+ throw err;
1463
+ }
1464
+ await this.processSSEStream(response, callbacks, signal, lifecycle);
1465
+ }, signal, {
1466
+ maxTotalDurationMs: this.config.retry?.maxTotalDurationMs ?? 10 * 6e4,
1467
+ maxTotalRetries: this.config.maxRetries,
1468
+ policy: this.config.retry,
1469
+ onRetry: (info) => {
1470
+ if (info.classified.category === "rate_limit") {
1471
+ callbacks.onRateLimit?.(info.classified.retryDelayMs);
1472
+ }
1473
+ }
1474
+ });
1475
+ }
1476
+ buildRequestBody(request) {
1477
+ const input = [];
1478
+ let instructions;
1479
+ const nonSystemMessages = request.messages.filter((m) => {
1480
+ if (m.role === "system") {
1481
+ instructions = (instructions ?? "") + m.content;
1482
+ return false;
1483
+ }
1484
+ return true;
1485
+ });
1486
+ for (const msg of nonSystemMessages) {
1487
+ if (msg.role === "user") {
1488
+ input.push({ type: "message", role: "user", content: [{ type: "input_text", text: msg.content }] });
1489
+ } else if (msg.role === "assistant") {
1490
+ const textContent = msg.content ?? "";
1491
+ if (textContent) {
1492
+ input.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: textContent }] });
1493
+ }
1494
+ if (msg.tool_calls) {
1495
+ for (const tc of msg.tool_calls) {
1496
+ input.push({
1497
+ type: "function_call",
1498
+ call_id: tc.id,
1499
+ name: tc.function.name,
1500
+ arguments: tc.function.arguments
1501
+ });
1502
+ }
1503
+ }
1504
+ } else if (msg.role === "tool") {
1505
+ input.push({
1506
+ type: "function_call_output",
1507
+ call_id: msg.tool_call_id,
1508
+ output: msg.content
1509
+ });
1510
+ }
1511
+ }
1512
+ const tools = request.tools?.map((t) => {
1513
+ const fn = t.function;
1514
+ const params = fn.parameters;
1515
+ const properties = params?.properties;
1516
+ const required = Array.isArray(params?.required) ? params.required : void 0;
1517
+ const schema = {
1518
+ type: "object",
1519
+ properties: properties ?? {}
1520
+ };
1521
+ if (required?.length) {
1522
+ schema.required = required;
1523
+ }
1524
+ if (params?.additionalProperties !== void 0) {
1525
+ schema.additionalProperties = params.additionalProperties;
1526
+ }
1527
+ return {
1528
+ type: "function",
1529
+ name: fn.name,
1530
+ description: fn.description,
1531
+ parameters: schema,
1532
+ strict: false
1533
+ };
1534
+ }) ?? [];
1535
+ const body = {
1536
+ model: this.config.model,
1537
+ input,
1538
+ stream: true,
1539
+ store: false,
1540
+ parallel_tool_calls: true,
1541
+ include: ["reasoning.encrypted_content"],
1542
+ reasoning: { effort: "high" }
1543
+ };
1544
+ if (instructions) {
1545
+ body.instructions = instructions;
1546
+ }
1547
+ if (tools.length > 0) {
1548
+ body.tools = tools;
1549
+ }
1550
+ if (request.tool_choice && tools.length > 0) {
1551
+ if (typeof request.tool_choice === "object") {
1552
+ const name = request.tool_choice.function.name;
1553
+ if (tools.some((t) => t.name === name)) {
1554
+ body.tool_choice = { type: "function", name };
1555
+ body.parallel_tool_calls = false;
1556
+ }
1557
+ } else {
1558
+ body.tool_choice = request.tool_choice;
1559
+ }
1560
+ }
1561
+ return body;
1562
+ }
1563
+ async processSSEStream(response, callbacks, signal, lifecycle) {
1564
+ const reader = response.body?.getReader();
1565
+ if (!reader) throw new Error("No response body");
1566
+ const decoder = new TextDecoder();
1567
+ let buffer = "";
1568
+ let stopReason = null;
1569
+ let usage;
1570
+ const functionCalls = /* @__PURE__ */ new Map();
1571
+ let pendingMessageItem = null;
1572
+ let seenReasoningItem = false;
1573
+ let seenTextDelta = false;
1574
+ const flushPendingMessage = () => {
1575
+ if (!pendingMessageItem) return;
1576
+ if (!seenTextDelta) {
1577
+ for (const t of pendingMessageItem.texts) {
1578
+ callbacks.onTextDelta(t);
1579
+ }
1580
+ }
1581
+ for (const b of pendingMessageItem.blocks) {
1582
+ callbacks.onContentBlock(b);
1583
+ }
1584
+ if (pendingMessageItem.msgUsage) {
1585
+ usage = {
1586
+ input_tokens: pendingMessageItem.msgUsage.input_tokens,
1587
+ output_tokens: pendingMessageItem.msgUsage.output_tokens,
1588
+ reasoning_tokens: extractReasoningTokens(pendingMessageItem.msgUsage)
1589
+ };
1590
+ }
1591
+ pendingMessageItem = null;
1592
+ };
1593
+ const FIRST_BYTE_TIMEOUT_MS2 = 18e4;
1594
+ const READ_TIMEOUT_MS2 = 3e5;
1595
+ const THINKING_STALL_TIMEOUT_MS = 9e4;
1596
+ let streamTimedOut = false;
1597
+ let idleTimer = null;
1598
+ let receivedFirstChunk = false;
1599
+ let receivedThinking = false;
1600
+ let textReceived = false;
1601
+ let receivedChars = 0;
1602
+ const timeoutController = new AbortController();
1603
+ const maxStreamMs = 10 * 6e4;
1604
+ const maxStreamTimer = setTimeout(() => timeoutController.abort(), maxStreamMs);
1605
+ const streamStartedAt = Date.now();
1606
+ const signalCleanup = signal ? wireAbortToReaderCancel(AbortSignal.any([signal, timeoutController.signal]), reader) : wireAbortToReaderCancel(timeoutController.signal, reader);
1607
+ const resetIdleTimer = () => {
1608
+ if (idleTimer) clearTimeout(idleTimer);
1609
+ const thinkingStallMs = receivedThinking && !textReceived ? THINKING_STALL_TIMEOUT_MS : null;
1610
+ const timeout = receivedFirstChunk ? thinkingStallMs ?? READ_TIMEOUT_MS2 : FIRST_BYTE_TIMEOUT_MS2;
1611
+ idleTimer = setTimeout(() => {
1612
+ streamTimedOut = true;
1613
+ reader.cancel().catch(() => {
1614
+ });
1615
+ }, timeout);
1616
+ };
1617
+ try {
1618
+ resetIdleTimer();
1619
+ while (true) {
1620
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
1621
+ if (timeoutController.signal.aborted) {
1622
+ throw new Error("Codex SSE stream hard timeout (10min) \u2014 stream exceeded maximum duration");
1623
+ }
1624
+ const { done, value } = await reader.read();
1625
+ if (streamTimedOut) throw new Error("Codex SSE stream idle timeout (300s)");
1626
+ if (done) break;
1627
+ receivedFirstChunk = true;
1628
+ buffer += decoder.decode(value, { stream: true });
1629
+ const lines = buffer.split("\n");
1630
+ buffer = lines.pop() ?? "";
1631
+ let sawDataEvent = false;
1632
+ for (const line of lines) {
1633
+ const trimmed = line.trim();
1634
+ if (!trimmed.startsWith("data: ")) continue;
1635
+ const data = trimmed.slice(6);
1636
+ if (data === "[DONE]") continue;
1637
+ sawDataEvent = true;
1638
+ let parsed;
1639
+ try {
1640
+ parsed = JSON.parse(data);
1641
+ } catch {
1642
+ continue;
1643
+ }
1644
+ const type = parsed.type;
1645
+ switch (type) {
1646
+ case "response.output_text.delta": {
1647
+ seenTextDelta = true;
1648
+ textReceived = true;
1649
+ const text = typeof parsed.delta === "string" ? parsed.delta : parsed.delta?.text;
1650
+ if (text) {
1651
+ receivedChars += text.length;
1652
+ callbacks.onTextDelta(text);
1653
+ }
1654
+ break;
1655
+ }
1656
+ case "response.reasoning_text.delta":
1657
+ case "response.reasoning_summary_text.delta": {
1658
+ seenReasoningItem = true;
1659
+ receivedThinking = true;
1660
+ const text = typeof parsed.delta === "string" ? parsed.delta : parsed.delta?.text;
1661
+ if (text) {
1662
+ receivedChars += text.length;
1663
+ callbacks.onThinkingDelta(text);
1664
+ }
1665
+ break;
1666
+ }
1667
+ case "response.output_item.added": {
1668
+ const item = parsed.item;
1669
+ if (item?.type === "function_call") callbacks.onToolCallDelta?.();
1670
+ break;
1671
+ }
1672
+ case "response.function_call_arguments.delta": {
1673
+ callbacks.onToolCallDelta?.();
1674
+ break;
1675
+ }
1676
+ case "response.output_item.done": {
1677
+ const item = parsed.item;
1678
+ if (item?.type === "function_call") {
1679
+ const callId = item.call_id ?? `call_${functionCalls.size}`;
1680
+ const name = item.name ?? "";
1681
+ const args = item.arguments ?? "";
1682
+ functionCalls.set(functionCalls.size, { id: callId, name, arguments: args });
1683
+ try {
1684
+ const input = JSON.parse(args);
1685
+ callbacks.onContentBlock({ type: "tool_use", id: callId, name, input });
1686
+ } catch {
1687
+ }
1688
+ } else if (item?.type === "reasoning") {
1689
+ seenReasoningItem = true;
1690
+ const summary = item.summary;
1691
+ if (summary) {
1692
+ for (const s of summary) {
1693
+ if (typeof s.text === "string") {
1694
+ callbacks.onThinkingDelta(s.text);
1695
+ }
1696
+ }
1697
+ }
1698
+ flushPendingMessage();
1699
+ } else if (item?.type === "message") {
1700
+ const content = item.content;
1701
+ const msgUsage = item.usage;
1702
+ if (!seenReasoningItem) {
1703
+ const texts = [];
1704
+ const blocks = [];
1705
+ if (content) {
1706
+ for (const part of content) {
1707
+ if (part.type === "output_text" && typeof part.text === "string") {
1708
+ texts.push(part.text);
1709
+ blocks.push({ type: "text", text: part.text });
1710
+ }
1711
+ }
1712
+ }
1713
+ pendingMessageItem = { texts, blocks, msgUsage };
1714
+ } else {
1715
+ if (content) {
1716
+ for (const part of content) {
1717
+ if (part.type === "output_text" && typeof part.text === "string") {
1718
+ if (!seenTextDelta) callbacks.onTextDelta(part.text);
1719
+ callbacks.onContentBlock({ type: "text", text: part.text });
1720
+ }
1721
+ }
1722
+ }
1723
+ if (msgUsage) {
1724
+ usage = {
1725
+ input_tokens: msgUsage.input_tokens,
1726
+ output_tokens: msgUsage.output_tokens,
1727
+ reasoning_tokens: extractReasoningTokens(msgUsage)
1728
+ };
1729
+ }
1730
+ }
1731
+ }
1732
+ break;
1733
+ }
1734
+ case "response.completed": {
1735
+ const resp = parsed.response;
1736
+ if (resp?.usage) {
1737
+ const u = resp.usage;
1738
+ usage = {
1739
+ input_tokens: u.input_tokens,
1740
+ output_tokens: u.output_tokens,
1741
+ reasoning_tokens: extractReasoningTokens(u)
1742
+ };
1743
+ }
1744
+ stopReason = "stop";
1745
+ break;
1746
+ }
1747
+ case "response.failed": {
1748
+ const resp = parsed.response;
1749
+ const error = resp?.error;
1750
+ const msg = error?.message ?? "Codex request failed";
1751
+ throw new Error(msg);
1752
+ }
1753
+ case "error": {
1754
+ const rawMsg = parsed.message ?? parsed.error;
1755
+ let msg;
1756
+ if (typeof rawMsg === "string") {
1757
+ msg = rawMsg;
1758
+ } else if (typeof rawMsg === "object" && rawMsg !== null) {
1759
+ const errObj = rawMsg;
1760
+ msg = errObj.message ?? errObj.error ?? JSON.stringify(errObj);
1761
+ } else {
1762
+ msg = "Unknown Codex error";
1763
+ }
1764
+ throw new Error(`Codex stream error: ${msg}`);
1765
+ }
1766
+ }
1767
+ }
1768
+ if (sawDataEvent) resetIdleTimer();
1769
+ }
1770
+ } catch (err) {
1771
+ callbacks.onStreamAttemptAborted?.({
1772
+ provider: "codex",
1773
+ receivedChars,
1774
+ elapsedMs: Date.now() - streamStartedAt,
1775
+ errorName: err?.name ?? "Error",
1776
+ errorMessage: err?.message ?? String(err)
1777
+ });
1778
+ throw wrapBodyTimeoutError(err, "Codex", streamStartedAt);
1779
+ } finally {
1780
+ if (idleTimer) clearTimeout(idleTimer);
1781
+ if (maxStreamTimer) clearTimeout(maxStreamTimer);
1782
+ if (signalCleanup) signalCleanup();
1783
+ reader.releaseLock();
1784
+ lifecycle?.abort();
1785
+ }
1786
+ flushPendingMessage();
1787
+ callbacks.onStopReason(stopReason ?? "stop", {
1788
+ input_tokens: usage?.input_tokens ?? 0,
1789
+ output_tokens: usage?.output_tokens ?? 0,
1790
+ cache_creation_input_tokens: 0,
1791
+ cache_read_input_tokens: 0,
1792
+ reasoning_tokens: usage?.reasoning_tokens
1793
+ });
1794
+ }
1795
+ };
1796
+
1797
+ // src/api/anthropic-client.ts
1798
+ init_esm_shims();
1799
+ var import_undici2 = __toESM(require_undici(), 1);
1800
+ function foldConsecutiveSameRole(messages) {
1801
+ const out = [];
1802
+ for (const msg of messages) {
1803
+ const prev = out[out.length - 1];
1804
+ if (prev && prev.role === msg.role) {
1805
+ prev.content.push(...msg.content);
1806
+ } else {
1807
+ out.push({ role: msg.role, content: [...msg.content] });
1808
+ }
1809
+ }
1810
+ return out;
1811
+ }
1812
+ var AnthropicClient = class {
1813
+ constructor(config) {
1814
+ this.config = config;
1815
+ this.proxyDispatcher = config.proxy ? new import_undici2.ProxyAgent(config.proxy) : void 0;
1816
+ }
1817
+ config;
1818
+ /** undici ProxyAgent for config.proxy (undefined = no per-provider proxy). */
1819
+ proxyDispatcher;
1820
+ setReasoningEffort(_effort) {
1821
+ }
1822
+ setThinking(_mode) {
1823
+ }
1824
+ async stream(request, callbacks, signal) {
1825
+ let stripRequested = false;
1826
+ let imagesStripped = false;
1827
+ await withStructuredRetry(async () => {
1828
+ let wireMessages = request.messages;
1829
+ if (stripRequested && !imagesStripped) {
1830
+ const stripped = stripOaiImageParts(wireMessages);
1831
+ if (stripped.removedCount > 0) {
1832
+ wireMessages = stripped.messages;
1833
+ imagesStripped = true;
1834
+ callbacks.onImageStripped?.({ removedCount: stripped.removedCount });
1835
+ }
1836
+ }
1837
+ const body = this.buildRequestBody({ ...request, messages: wireMessages });
1838
+ const lifecycle = new AbortController();
1839
+ if (signal) {
1840
+ if (signal.aborted) lifecycle.abort();
1841
+ else signal.addEventListener("abort", () => lifecycle.abort(), { once: true });
1842
+ }
1843
+ await acquireRateLimitSlot(this.config.baseUrl, this.config.retry?.rateLimit, lifecycle.signal);
1844
+ const response = await fetchWithTimeout(`${normalizeBaseUrl(this.config.baseUrl)}/v1/messages`, {
1845
+ method: "POST",
1846
+ headers: {
1847
+ "Content-Type": "application/json",
1848
+ "x-api-key": this.config.apiKey,
1849
+ "anthropic-version": "2023-06-01",
1850
+ "Accept": "text/event-stream",
1851
+ ...this.config.userAgent ? { "User-Agent": this.config.userAgent } : {},
1852
+ ...this.config.sessionId ? { [this.config.sessionHeader ?? "X-Request-Session"]: this.config.sessionId } : {}
1853
+ },
1854
+ body: JSON.stringify(body),
1855
+ signal: lifecycle.signal
1856
+ }, this.config.thinkingBudget && this.config.thinkingBudget > 0 ? 9e4 : 45e3, this.proxyDispatcher);
1857
+ if (!response.ok) {
1858
+ const errorBody = await response.text().catch(() => "");
1859
+ const err = Object.assign(
1860
+ new Error(`Anthropic API error (${response.status}): ${errorBody}`),
1861
+ {
1862
+ status: response.status,
1863
+ // 413 的两种成因在 wire 层同形——只有这里知道刚发出去的体里有没有图。
1864
+ // 标在错误上让分类器分流(无图 = 纯上下文超限,重发无用)。
1865
+ ...response.status === 413 ? { payloadHadImages: oaiMessagesHaveImageParts(wireMessages) } : {}
1866
+ }
1867
+ );
1868
+ const retryAfter = response.headers.get("retry-after");
1869
+ if (retryAfter) {
1870
+ const retryAfterMs = parseRetryAfterMs(retryAfter);
1871
+ if (retryAfterMs !== void 0) {
1872
+ ;
1873
+ err.retryAfterMs = retryAfterMs;
1874
+ }
1875
+ }
1876
+ throw err;
1877
+ }
1878
+ await this.processSSEStream(response, callbacks, signal, lifecycle);
1879
+ }, signal, {
1880
+ maxTotalDurationMs: this.config.retry?.maxTotalDurationMs ?? 10 * 6e4,
1881
+ // provider 级 maxRetries 显式配置时覆盖内置默认(0 = 禁用重试)。
1882
+ maxTotalRetries: this.config.maxRetries,
1883
+ policy: this.config.retry,
1884
+ onRetry: (info) => {
1885
+ if (info.classified.category === "rate_limit") {
1886
+ callbacks.onRateLimit?.(info.classified.retryDelayMs);
1887
+ }
1888
+ if (info.classified.category === "image_strip") {
1889
+ stripRequested = true;
1890
+ }
1891
+ }
1892
+ });
1893
+ }
1894
+ /** Exposed for testing. */
1895
+ buildRequestBodyForTest(request) {
1896
+ return this.buildRequestBody(request);
1897
+ }
1898
+ buildRequestBody(request) {
1899
+ let systemText = "";
1900
+ const nonSystemMessages = request.messages.filter((m) => {
1901
+ if (m.role === "system") {
1902
+ systemText += (systemText ? "\n\n" : "") + m.content;
1903
+ return false;
1904
+ }
1905
+ return true;
1906
+ });
1907
+ const system = systemText ? [{ type: "text", text: systemText }] : [];
1908
+ const messages = foldConsecutiveSameRole(nonSystemMessages.map((m) => this.convertMessage(m)));
1909
+ const tools = request.tools && request.tools.length > 0 ? [...request.tools].sort((a, b) => a.function.name.localeCompare(b.function.name)).map((t) => ({
1910
+ name: t.function.name,
1911
+ description: t.function.description,
1912
+ input_schema: t.function.parameters
1913
+ })) : void 0;
1914
+ const body = {
1915
+ // 空 model 回退到 client 绑定值(侧路调用契约,同 openai-client)。
1916
+ model: request.model || this.config.model,
1917
+ max_tokens: request.max_tokens ?? this.config.maxTokens,
1918
+ messages,
1919
+ stream: true
1920
+ };
1921
+ if (system.length > 0) body.system = system;
1922
+ if (tools) body.tools = tools;
1923
+ if (this.config.thinkingBudget && this.config.thinkingBudget > 0) {
1924
+ body.thinking = { type: "enabled", budget_tokens: this.config.thinkingBudget };
1925
+ } else if (this.config.temperature !== void 0) {
1926
+ body.temperature = Math.max(0, Math.min(1, this.config.temperature));
1927
+ }
1928
+ if (request.tool_choice && typeof request.tool_choice === "object" && tools) {
1929
+ const name = request.tool_choice.function.name;
1930
+ if (tools.some((t) => t.name === name)) {
1931
+ body.tool_choice = { type: "tool", name };
1932
+ }
1933
+ }
1934
+ if (tools && tools.length > 0) {
1935
+ tools[tools.length - 1].cache_control = { type: "ephemeral", ttl: "1h" };
1936
+ }
1937
+ if (system.length > 0) {
1938
+ system[system.length - 1].cache_control = { type: "ephemeral", ttl: "1h" };
1939
+ }
1940
+ let firstUserIdx = -1;
1941
+ for (let i = 0; i < messages.length; i++) {
1942
+ if (messages[i].role === "user") {
1943
+ firstUserIdx = i;
1944
+ break;
1945
+ }
1946
+ }
1947
+ if (firstUserIdx >= 0) {
1948
+ const blocks = messages[firstUserIdx].content;
1949
+ if (blocks.length > 0) {
1950
+ blocks[blocks.length - 1].cache_control = { type: "ephemeral" };
1951
+ }
1952
+ }
1953
+ const MAX_LOOKBACK = 15;
1954
+ let totalBlocks = 0;
1955
+ for (const msg of messages) {
1956
+ totalBlocks += msg.content.length;
1957
+ }
1958
+ let bp4Idx = -1;
1959
+ let blockPos = 0;
1960
+ for (let i = 0; i < messages.length; i++) {
1961
+ blockPos += messages[i].content.length;
1962
+ if (messages[i].role === "assistant" && i !== firstUserIdx) {
1963
+ const fromEnd = totalBlocks - blockPos;
1964
+ if (fromEnd < MAX_LOOKBACK) {
1965
+ bp4Idx = i;
1966
+ break;
1967
+ }
1968
+ }
1969
+ }
1970
+ if (bp4Idx >= 0) {
1971
+ const bp4Blocks = messages[bp4Idx].content;
1972
+ if (bp4Blocks.length > 0) {
1973
+ bp4Blocks[bp4Blocks.length - 1].cache_control = { type: "ephemeral" };
1974
+ }
1975
+ }
1976
+ return body;
1977
+ }
1978
+ convertMessage(msg) {
1979
+ if (msg.role === "user") {
1980
+ if (Array.isArray(msg.content)) {
1981
+ return {
1982
+ role: "user",
1983
+ content: msg.content.map((p) => {
1984
+ if (p.type === "text") return { type: "text", text: p.text };
1985
+ const url = p.image_url.url;
1986
+ const m = url.match(/^data:(image\/[a-z+]+);base64,(.+)$/);
1987
+ if (m) return { type: "image", source: { type: "base64", media_type: m[1], data: m[2] } };
1988
+ return { type: "text", text: `[unsupported image: ${url.slice(0, 50)}]` };
1989
+ })
1990
+ };
1991
+ }
1992
+ return {
1993
+ role: "user",
1994
+ content: [{ type: "text", text: msg.content }]
1995
+ };
1996
+ }
1997
+ if (msg.role === "tool") {
1998
+ return {
1999
+ role: "user",
2000
+ content: [{ type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }]
2001
+ };
2002
+ }
2003
+ if (msg.role === "assistant") {
2004
+ const blocks = [];
2005
+ let text = msg.content ?? "";
2006
+ if (msg.reasoning_content) {
2007
+ text = `<thinking>
2008
+ ${msg.reasoning_content}
2009
+ </thinking>
2010
+
2011
+ ${text}`;
2012
+ }
2013
+ if (text) {
2014
+ blocks.push({ type: "text", text });
2015
+ }
2016
+ if (msg.tool_calls) {
2017
+ for (const tc of msg.tool_calls) {
2018
+ const input = parseJsonObjectWithEscapeRepair(tc.function.arguments) ?? {};
2019
+ blocks.push({
2020
+ type: "tool_use",
2021
+ id: tc.id,
2022
+ name: tc.function.name,
2023
+ input
2024
+ });
2025
+ }
2026
+ }
2027
+ return { role: "assistant", content: blocks };
2028
+ }
2029
+ throw new Error(`Unsupported message role in AnthropicClient: ${JSON.stringify(msg)}`);
2030
+ }
2031
+ async processSSEStream(response, callbacks, signal, lifecycle) {
2032
+ const reader = response.body?.getReader();
2033
+ if (!reader) throw new Error("No response body");
2034
+ const decoder = new TextDecoder();
2035
+ let buffer = "";
2036
+ let stopReason = null;
2037
+ let usage = {};
2038
+ const textBlocks = [];
2039
+ const thinkingBlocks = [];
2040
+ const toolUseBuffer = /* @__PURE__ */ new Map();
2041
+ const FIRST_BYTE_TIMEOUT_MS2 = this.config.thinkingBudget && this.config.thinkingBudget > 0 ? 9e4 : 45e3;
2042
+ const READ_TIMEOUT_MS2 = this.config.thinkingBudget && this.config.thinkingBudget > 0 ? 18e4 : 12e4;
2043
+ let streamTimedOut = false;
2044
+ let idleTimer = null;
2045
+ let receivedFirstChunk = false;
2046
+ let receivedChars = 0;
2047
+ const timeoutController = new AbortController();
2048
+ const maxStreamMs = this.config.requestTimeoutMs ?? 10 * 6e4;
2049
+ const maxStreamTimer = setTimeout(() => timeoutController.abort(), maxStreamMs);
2050
+ const streamStartedAt = Date.now();
2051
+ const signalCleanup = signal ? wireAbortToReaderCancel(AbortSignal.any([signal, timeoutController.signal]), reader) : wireAbortToReaderCancel(timeoutController.signal, reader);
2052
+ const resetIdleTimer = () => {
2053
+ if (idleTimer) clearTimeout(idleTimer);
2054
+ const timeout = receivedFirstChunk ? READ_TIMEOUT_MS2 : FIRST_BYTE_TIMEOUT_MS2;
2055
+ idleTimer = setTimeout(() => {
2056
+ streamTimedOut = true;
2057
+ reader.cancel().catch(() => {
2058
+ });
2059
+ }, timeout);
2060
+ };
2061
+ try {
2062
+ resetIdleTimer();
2063
+ const throwIfHardCapAborted = () => {
2064
+ if (!timeoutController.signal.aborted) return;
2065
+ throw new Error(this.config.requestTimeoutMs !== void 0 ? `Anthropic SSE stream request timeout (${Math.round(this.config.requestTimeoutMs / 1e3)}s, provider requestTimeoutMs) \u2014 total request duration exceeded configured limit` : "Anthropic SSE stream hard timeout (10min) \u2014 stream exceeded maximum duration");
2066
+ };
2067
+ while (true) {
2068
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
2069
+ throwIfHardCapAborted();
2070
+ const { done, value } = await reader.read();
2071
+ if (streamTimedOut) throw new Error("Anthropic SSE stream idle timeout (180s)");
2072
+ if (done) {
2073
+ throwIfHardCapAborted();
2074
+ break;
2075
+ }
2076
+ receivedFirstChunk = true;
2077
+ buffer += decoder.decode(value, { stream: true });
2078
+ const lines = buffer.split("\n");
2079
+ buffer = lines.pop() ?? "";
2080
+ let sawContentEvent = false;
2081
+ for (const line of lines) {
2082
+ const trimmed = line.trim();
2083
+ if (trimmed.startsWith("event: ")) continue;
2084
+ if (!trimmed.startsWith("data:")) continue;
2085
+ const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5);
2086
+ let parsed;
2087
+ try {
2088
+ parsed = JSON.parse(data);
2089
+ } catch {
2090
+ continue;
2091
+ }
2092
+ const type = parsed.type;
2093
+ if (type !== "ping") sawContentEvent = true;
2094
+ switch (type) {
2095
+ case "message_start": {
2096
+ const msg = parsed.message;
2097
+ if (msg?.usage) {
2098
+ const u = msg.usage;
2099
+ usage = {
2100
+ input_tokens: u.input_tokens,
2101
+ cache_read_input_tokens: u.cache_read_input_tokens,
2102
+ cache_creation_input_tokens: u.cache_creation_input_tokens
2103
+ };
2104
+ }
2105
+ break;
2106
+ }
2107
+ case "content_block_start": {
2108
+ const index = parsed.index;
2109
+ const block = parsed.content_block;
2110
+ if (!block) break;
2111
+ if (block.type === "tool_use" && index !== void 0) {
2112
+ callbacks.onToolCallDelta?.();
2113
+ const id = block.id;
2114
+ const name = block.name;
2115
+ if (id && name) {
2116
+ toolUseBuffer.set(index, { id, name, partialJson: "" });
2117
+ }
2118
+ }
2119
+ break;
2120
+ }
2121
+ case "content_block_delta": {
2122
+ const index = parsed.index;
2123
+ const delta = parsed.delta;
2124
+ if (!delta) break;
2125
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
2126
+ receivedChars += delta.text.length;
2127
+ textBlocks.push(delta.text);
2128
+ callbacks.onTextDelta(delta.text);
2129
+ } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
2130
+ receivedChars += delta.thinking.length;
2131
+ thinkingBlocks.push(delta.thinking);
2132
+ callbacks.onThinkingDelta(delta.thinking);
2133
+ } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && index !== void 0) {
2134
+ const buf = toolUseBuffer.get(index);
2135
+ if (buf) {
2136
+ buf.partialJson += delta.partial_json;
2137
+ }
2138
+ }
2139
+ break;
2140
+ }
2141
+ case "content_block_stop": {
2142
+ const index = parsed.index;
2143
+ if (index !== void 0) {
2144
+ const buf = toolUseBuffer.get(index);
2145
+ if (buf) {
2146
+ let input = {};
2147
+ let argsTruncated;
2148
+ const repaired = parseJsonObjectWithEscapeRepair(buf.partialJson || "{}");
2149
+ if (repaired !== null) {
2150
+ input = repaired;
2151
+ } else {
2152
+ argsTruncated = true;
2153
+ }
2154
+ callbacks.onContentBlock({ type: "tool_use", id: buf.id, name: buf.name, input, argsTruncated });
2155
+ toolUseBuffer.delete(index);
2156
+ }
2157
+ }
2158
+ break;
2159
+ }
2160
+ case "message_delta": {
2161
+ const d = parsed.delta;
2162
+ if (d?.stop_reason) {
2163
+ stopReason = d.stop_reason;
2164
+ }
2165
+ if (parsed.usage) {
2166
+ const u = parsed.usage;
2167
+ usage.output_tokens = u.output_tokens;
2168
+ }
2169
+ break;
2170
+ }
2171
+ case "message_stop": {
2172
+ break;
2173
+ }
2174
+ case "error": {
2175
+ const err = parsed.error;
2176
+ throw new Error(`Anthropic stream error: ${err?.message ?? "Unknown error"}`);
2177
+ }
2178
+ }
2179
+ }
2180
+ if (sawContentEvent) resetIdleTimer();
2181
+ }
2182
+ } catch (err) {
2183
+ callbacks.onStreamAttemptAborted?.({
2184
+ provider: "anthropic",
2185
+ receivedChars,
2186
+ elapsedMs: Date.now() - streamStartedAt,
2187
+ errorName: err?.name ?? "Error",
2188
+ errorMessage: err?.message ?? String(err)
2189
+ });
2190
+ throw wrapBodyTimeoutError(err, "Anthropic", streamStartedAt);
2191
+ } finally {
2192
+ if (idleTimer) clearTimeout(idleTimer);
2193
+ if (maxStreamTimer) clearTimeout(maxStreamTimer);
2194
+ if (signalCleanup) signalCleanup();
2195
+ reader.releaseLock();
2196
+ lifecycle?.abort();
2197
+ }
2198
+ if (textBlocks.length > 0) {
2199
+ callbacks.onContentBlock({ type: "text", text: textBlocks.join("") });
2200
+ }
2201
+ if (thinkingBlocks.length > 0) {
2202
+ callbacks.onContentBlock({ type: "thinking", thinking: thinkingBlocks.join("") });
2203
+ }
2204
+ const rawInput = usage.input_tokens ?? 0;
2205
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
2206
+ const cacheCreation = usage.cache_creation_input_tokens ?? 0;
2207
+ callbacks.onStopReason(stopReason ?? "end_turn", {
2208
+ input_tokens: rawInput + cacheRead + cacheCreation,
2209
+ output_tokens: usage.output_tokens ?? 0,
2210
+ cache_read_input_tokens: cacheRead,
2211
+ cache_creation_input_tokens: cacheCreation
2212
+ });
2213
+ }
2214
+ };
2215
+
2216
+ // src/api/provider-catalog.ts
2217
+ init_esm_shims();
2218
+ var TIANSHU_USER_AGENT = `tianshu-harness/${"3.23.0"}`;
2219
+ var CATALOG_META = {
2220
+ deepseek: {
2221
+ label: "DeepSeek",
2222
+ wire: { thinkingStallTimeoutMs: 12e4 },
2223
+ notes: [
2224
+ "Exact-prefix cache: first 2 messages must remain stable for 99% hit rate",
2225
+ "Thinking block: {type: enabled} + reasoning_effort"
2226
+ ]
2227
+ },
2228
+ kimi: {
2229
+ label: "Kimi (Moonshot)",
2230
+ wire: { userAgent: "KimiCLI/1.0" },
2231
+ notes: [
2232
+ "Thinking block: {type: enabled} + reasoning_effort",
2233
+ "K3 / K3-256K: reasoning_effort low|high|max (default high); K2.7 Code is Thinking:ON only",
2234
+ "No prefix cache support"
2235
+ ]
2236
+ },
2237
+ glm: {
2238
+ label: "GLM (Zhipu)",
2239
+ wire: { thinkingStallTimeoutMs: 42e4 },
2240
+ notes: [
2241
+ "OpenAI-compatible protocol via /api/paas/v4",
2242
+ "Thinking block: {type: enabled} + reasoning_effort",
2243
+ "Implicit exact-prefix cache (cached_tokens in prompt_tokens_details), 1M context"
2244
+ ]
2245
+ },
2246
+ minimax: {
2247
+ label: "MiniMax",
2248
+ wire: { useMaxCompletionTokens: true },
2249
+ notes: [
2250
+ "MiniMax-M3: 1M context, multimodal (image/video)",
2251
+ "Uses max_completion_tokens instead of max_tokens",
2252
+ "Thinking block: {type: adaptive} + reasoning_split",
2253
+ "No prefix cache support"
2254
+ ]
2255
+ },
2256
+ mimo: {
2257
+ label: "Mimo",
2258
+ wire: { useMaxCompletionTokens: true },
2259
+ notes: [
2260
+ "Thinking block: {type: enabled}",
2261
+ "No thinking effort control",
2262
+ "No cache support"
2263
+ ]
2264
+ },
2265
+ "mimo-api": {
2266
+ label: "MiMo API",
2267
+ wire: { useMaxCompletionTokens: true },
2268
+ notes: [
2269
+ "Pay-as-you-go MiMo API endpoint",
2270
+ "Thinking block: {type: enabled}",
2271
+ "Exact-prefix cache like DeepSeek"
2272
+ ]
2273
+ },
2274
+ "opencode-go": {
2275
+ label: "OpenCode Go",
2276
+ // name 层兜底:host 规则覆盖直连(也覆盖手工配置的 anthropic 形态),但用户
2277
+ // 经本地中转 / 自建网关(baseUrl 不是 opencode.ai)时 host 不匹配——只要名字
2278
+ // 还认得出是 OpenCode Go,头就该照发。
2279
+ wire: { userAgent: TIANSHU_USER_AGENT, sessionHeader: "x-opencode-session" },
2280
+ notes: [
2281
+ "No thinking block \u2014 reasoning arrives as reasoning_content with no params needed",
2282
+ "reasoning_effort is accepted (low|medium|high|max|xhigh, all 200) and passed through verbatim",
2283
+ "No cache support",
2284
+ "Upstream requires x-opencode-session (stable per conversation) + a non-generic User-Agent; wire rules are host-keyed in HOST_WIRE_RULES"
2285
+ ]
2286
+ },
2287
+ "opencode-go-anthropic": {
2288
+ label: "OpenCode Go (Anthropic \u534F\u8BAE)",
2289
+ wire: { userAgent: TIANSHU_USER_AGENT, sessionHeader: "x-opencode-session" },
2290
+ notes: [
2291
+ "Anthropic /v1/messages endpoint of the same upstream",
2292
+ "Same session/UA requirements as opencode-go"
2293
+ ]
2294
+ },
2295
+ openai: {
2296
+ label: "OpenAI",
2297
+ notes: [
2298
+ "Partial-prefix cache with 128-token granularity",
2299
+ "Ephemeral cache (5 min TTL)"
2300
+ ]
2301
+ },
2302
+ codex: {
2303
+ label: "Codex",
2304
+ notes: [
2305
+ "Uses Codex Responses API with OAuth authentication",
2306
+ "Partial-prefix cache profile is provided by provider-profile.ts"
2307
+ ]
2308
+ },
2309
+ claude: {
2310
+ label: "Claude (Anthropic)",
2311
+ notes: [
2312
+ "Thinking block: {type: enabled} + budget_tokens",
2313
+ "Reasoning effort via reasoning_effort parameter"
2314
+ ]
2315
+ },
2316
+ longcat: {
2317
+ label: "LongCat (Meituan)",
2318
+ notes: [
2319
+ "API supports only model/messages/stream/max_tokens/temperature/top_p \u2014 no response_format",
2320
+ "Implicit server-side exact-prefix cache, cache hits free (official pricing)",
2321
+ "JSON repair must run as plain-text re-ask (no json-mode)"
2322
+ ]
2323
+ },
2324
+ ccswitch: {
2325
+ label: "CC Switch",
2326
+ notes: [
2327
+ `\u672C\u5730 OpenAI \u517C\u5BB9\u4EE3\u7406\uFF0C\u9ED8\u8BA4 ${process.env.CC_SWITCH_PROXY_URL ?? "http://127.0.0.1:8891/v1"}`,
2328
+ "\u8BA4\u8BC1\u4F7F\u7528 CC_SWITCH_PROXY_API_KEY \u73AF\u5883\u53D8\u91CF",
2329
+ "\u6A21\u578B\u540D\u900F\u4F20\u2014\u2014\u53EF\u7528\u6A21\u578B\u53D6\u51B3\u4E8E cc-switch \u4FA7\u914D\u7F6E",
2330
+ "\u26A0\uFE0F \u9884\u8BBE\u6A21\u578B\u5217\u8868\u4E3A\u79CD\u5B50\uFF0C\u4F7F\u7528\u524D\u786E\u8BA4\u6A21\u578B\u540D\u4E0E cc-switch \u4FA7\u4E00\u81F4\uFF0C\u5426\u5219\u4F1A 4xx",
2331
+ "\u652F\u6301 reasoning_effort\uFF08OpenAI \u683C\u5F0F\uFF09\uFF0CRectifier \u5C42\u7FFB\u8BD1\u4E3A\u4E0A\u6E38\u539F\u751F\u63A8\u7406\u683C\u5F0F"
2332
+ ]
2333
+ },
2334
+ siliconflow: {
2335
+ label: "\u7845\u57FA\u6D41\u52A8 (SiliconFlow)",
2336
+ notes: [
2337
+ "\u805A\u5408\u7AD9\uFF1A\u591A\u6A21\u578B\u53EF\u9009\uFF08DeepSeek / GLM / Kimi / Qwen\uFF09",
2338
+ "Thinking block: {type: enabled} + reasoning_effort\uFF08\u4E0A\u6E38\u652F\u6301\uFF09",
2339
+ "\u670D\u52A1\u7AEF\u9690\u5F0F exact-prefix \u7F13\u5B58\uFF08DeepSeek-V4 / GLM-5.2 \u8BA1 Cached Input \u4EF7\uFF09",
2340
+ "\u9ED8\u8BA4\u6A21\u578B\u662F DeepSeek \u4EE3\u7406 \u2192 \u6CBF\u7528 toolJsonBug \u6807\u8BB0"
2341
+ ]
2342
+ },
2343
+ dashscope: {
2344
+ label: "\u901A\u4E49\u5343\u95EE (DashScope)",
2345
+ notes: [
2346
+ "\u963F\u91CC DashScope\uFF1AQwen \u7CFB\u5217\u5B98\u65B9 OpenAI \u517C\u5BB9\u7AEF\u70B9",
2347
+ "Thinking block: {type: enabled} + reasoning_effort\uFF08Qwen3-max \u652F\u6301\uFF09",
2348
+ "\u6A21\u578B\u80FD\u529B\u5206\u88C2\uFF1AQwen-plus/turbo \u4E0D\u652F\u6301 thinking \u2192 \u7528 models[].capabilities override",
2349
+ "\u4E0D\u652F\u6301 cache_control breakpoint\uFF08\u975E Anthropic \u534F\u8BAE\uFF09"
2350
+ ]
2351
+ },
2352
+ openrouter: {
2353
+ label: "OpenRouter",
2354
+ notes: [
2355
+ "\u56FD\u9645\u805A\u5408\uFF1AOpenAI / Claude / Gemini / \u5F00\u6E90\u6A21\u578B",
2356
+ "thinking block \u900F\u4F20\u4E0D\u7A33\u5B9A \u2192 thinkingBlockType=none\uFF0C\u4EC5\u900F\u4F20 reasoning_effort",
2357
+ "\u7528\u6237\u53EF\u6309\u9700\u5728 models[].capabilities \u91CC\u4E3A\u7279\u5B9A\u6A21\u578B\u5F00\u542F thinking"
2358
+ ]
2359
+ },
2360
+ relay: {
2361
+ label: "\u81EA\u5EFA\u4E2D\u8F6C (one-api / new-api)",
2362
+ notes: [
2363
+ "\u901A\u7528 OpenAI \u517C\u5BB9\u4E2D\u8F6C\u6A21\u677F\uFF08one-api / new-api / litellm \u7B49\uFF09",
2364
+ "\u9ED8\u8BA4 config \u4E0D\u6FC0\u6D3B\u2014\u2014\u7528\u6237\u624B\u52A8\u542F\u7528\uFF0C\u907F\u514D\u548C ccswitch \u4F53\u9A8C\u91CD\u590D",
2365
+ "baseUrl \u8D70 RELAY_BASE_URL \u73AF\u5883\u53D8\u91CF\uFF1B\u6A21\u578B\u5217\u8868\u6309\u9700\u586B\u5199",
2366
+ "\u4E0E ccswitch \u540C\u6A21\u677F\uFF1Areasoning_effort \u900F\u4F20\uFF0CRectifier \u7FFB\u8BD1\u4E3A\u4E0A\u6E38\u539F\u751F\u683C\u5F0F"
2367
+ ]
2368
+ }
2369
+ };
2370
+ var HOST_WIRE_RULES = [
2371
+ {
2372
+ host: "opencode.ai",
2373
+ wire: { userAgent: TIANSHU_USER_AGENT, sessionHeader: "x-opencode-session" }
2374
+ }
2375
+ ];
2376
+ function hostWireRule(baseUrl) {
2377
+ if (!baseUrl) return void 0;
2378
+ let host;
2379
+ try {
2380
+ host = new URL(baseUrl).hostname.toLowerCase().replace(/\.$/, "");
2381
+ } catch {
2382
+ return void 0;
2383
+ }
2384
+ return HOST_WIRE_RULES.find((r) => host === r.host || host.endsWith(`.${r.host}`))?.wire;
2385
+ }
2386
+ function resolveProviderWire(name, baseUrl) {
2387
+ const byName = CATALOG_META[name]?.wire;
2388
+ const byHost = hostWireRule(baseUrl);
2389
+ if (!byName && !byHost) return void 0;
2390
+ return { ...byHost, ...byName };
2391
+ }
2392
+ function buildEntry(key) {
2393
+ const meta = CATALOG_META[key];
2394
+ return {
2395
+ key,
2396
+ label: meta?.label ?? key,
2397
+ // Live reference — capability data lives in provider.ts WELL_KNOWN_DEFAULTS.
2398
+ capabilities: WELL_KNOWN_DEFAULTS[key],
2399
+ wire: meta?.wire,
2400
+ notes: meta?.notes ?? []
2401
+ };
2402
+ }
2403
+ var PROVIDER_CATALOG = Object.fromEntries(
2404
+ Object.keys(WELL_KNOWN_DEFAULTS).map((key) => [key, buildEntry(key)])
2405
+ );
2406
+ var providerEntrySchema = external_exports.object({
2407
+ key: external_exports.string().min(1),
2408
+ label: external_exports.string().min(1),
2409
+ capabilities: external_exports.object({
2410
+ supportsThinking: external_exports.boolean(),
2411
+ thinkingBlockType: external_exports.enum(["enabled", "adaptive", "none"]),
2412
+ preservedThinkingProtocol: external_exports.boolean().optional(),
2413
+ reasoningSplit: external_exports.boolean().optional(),
2414
+ thinkingBudgetField: external_exports.enum(["budget_tokens"]).optional(),
2415
+ effortCap: external_exports.record(external_exports.string(), external_exports.string()).optional(),
2416
+ supportsCacheControl: external_exports.boolean(),
2417
+ stripParams: external_exports.array(external_exports.string()),
2418
+ hasToolJsonInContentBug: external_exports.boolean(),
2419
+ effortFormat: external_exports.enum(["reasoning_effort", "output_config", "none"]),
2420
+ prefixCacheStrategy: external_exports.enum(["deepseek-native", "anthropic-cache-control", "none"]),
2421
+ supportsResponseFormat: external_exports.boolean()
2422
+ }),
2423
+ cacheProfile: external_exports.object({
2424
+ cacheType: external_exports.enum(["exact-prefix", "explicit-breakpoint", "partial-prefix", "block-kv", "none"]),
2425
+ persistent: external_exports.boolean(),
2426
+ minCacheTokens: external_exports.number().int().nonnegative(),
2427
+ cacheGranularity: external_exports.number().int().positive().optional(),
2428
+ ttlSeconds: external_exports.number().int().positive().optional()
2429
+ }),
2430
+ hasUsageMapping: external_exports.boolean(),
2431
+ notes: external_exports.array(external_exports.string()).default([])
2432
+ });
2433
+
2434
+ // src/api/caller-identity.ts
2435
+ init_esm_shims();
2436
+ import { randomUUID } from "crypto";
2437
+ var PROCESS_SESSION_ID = `tianshu-${randomUUID()}`;
2438
+ function callerIdentityHeaders(wire, sessionId) {
2439
+ const headers = {};
2440
+ if (wire?.userAgent) headers["User-Agent"] = wire.userAgent;
2441
+ if (wire?.sessionHeader) {
2442
+ headers[wire.sessionHeader] = sessionId ?? PROCESS_SESSION_ID;
2443
+ } else if (sessionId) {
2444
+ headers["X-Request-Session"] = sessionId;
2445
+ }
2446
+ return headers;
2447
+ }
2448
+ function providerIdentityHeaders(providerName, baseUrl, sessionId) {
2449
+ return callerIdentityHeaders(resolveProviderWire(providerName ?? "", baseUrl), sessionId);
2450
+ }
2451
+
2452
+ // src/api/factory.ts
2453
+ function resolveApiKey(provider) {
2454
+ const cred = { name: provider.name, keyRef: provider.keyRef, apiKey: provider.apiKey, apiKeyEnv: provider.apiKeyEnv };
2455
+ const resolved = tryResolveCredentialKey(cred);
2456
+ if (resolved !== void 0) return resolved;
2457
+ if (isKeylessProviderEntry(provider.name, provider)) return "";
2458
+ throw new Error(
2459
+ `No API key configured for provider "${provider.name}". Set apiKey in config or the ${provider.apiKeyEnv ?? `${provider.name.toUpperCase()}_API_KEY`} environment variable.`
2460
+ );
2461
+ }
2462
+ function tryResolveCredentialKey(cred) {
2463
+ const { name, keyRef, apiKey, apiKeyEnv } = cred;
2464
+ if (keyRef) {
2465
+ const secret = readSecret(keyRef);
2466
+ if (secret) return secret;
2467
+ }
2468
+ if (apiKey) return apiKey;
2469
+ if (apiKeyEnv) {
2470
+ const env2 = process.env[apiKeyEnv];
2471
+ if (env2) return env2;
2472
+ }
2473
+ const env = process.env[`${name.toUpperCase()}_API_KEY`];
2474
+ if (env) return env;
2475
+ return void 0;
2476
+ }
2477
+ function resolveCredentialKey(cred) {
2478
+ const resolved = tryResolveCredentialKey(cred);
2479
+ if (resolved !== void 0) return resolved;
2480
+ const defaultEnvVar = `${cred.name.toUpperCase()}_API_KEY`;
2481
+ throw new Error(
2482
+ `No API key configured for "${cred.name}". Set apiKey in config or the ${cred.apiKeyEnv ?? defaultEnvVar} environment variable.`
2483
+ );
2484
+ }
2485
+ function createProviderClient(provider, capabilities, params) {
2486
+ const wire = resolveProviderWire(provider.name, provider.baseUrl);
2487
+ const explicitSessionId = params.sessionId?.trim() ? params.sessionId : void 0;
2488
+ const sessionId = explicitSessionId ?? (wire?.sessionHeader ? PROCESS_SESSION_ID : void 0);
2489
+ const proFactory = proRegistry.getClientFactory(provider.name);
2490
+ if (proFactory) return proFactory(provider, capabilities, params);
2491
+ if (provider.name === "codex" && provider.auth?.type === "oauth") {
2492
+ return new CodexClient({
2493
+ baseUrl: provider.baseUrl,
2494
+ model: params.model,
2495
+ maxTokens: params.maxTokens,
2496
+ auth: params.auth,
2497
+ maxRetries: provider.maxRetries,
2498
+ retry: provider.retry
2499
+ });
2500
+ }
2501
+ if (provider.protocol === "anthropic") {
2502
+ const budgetMap = {
2503
+ max: params.maxTokens,
2504
+ high: Math.floor(params.maxTokens * 0.6),
2505
+ medium: Math.floor(params.maxTokens * 0.3),
2506
+ low: 8192
2507
+ };
2508
+ const thinkingBudget = params.reasoningEffort ? budgetMap[params.reasoningEffort] ?? Math.floor(params.maxTokens * 0.6) : void 0;
2509
+ return new AnthropicClient({
2510
+ baseUrl: provider.baseUrl,
2511
+ apiKey: params.apiKey,
2512
+ model: params.model,
2513
+ maxTokens: params.maxTokens,
2514
+ thinkingBudget,
2515
+ requestTimeoutMs: provider.requestTimeoutMs,
2516
+ maxRetries: provider.maxRetries,
2517
+ retry: provider.retry,
2518
+ temperature: provider.temperature,
2519
+ proxy: provider.proxy,
2520
+ userAgent: wire?.userAgent,
2521
+ sessionId,
2522
+ sessionHeader: wire?.sessionHeader
2523
+ });
2524
+ }
2525
+ return new OpenAIClient({
2526
+ // 用户在 Base URL 里粘贴 curl 全文(`…/v1/chat/completions`)或留个尾斜杠
2527
+ // 是常态。发送路径拼的是 `${baseUrl}/chat/completions`,不归一化就会出现
2528
+ // `…/chat/completions/chat/completions`(404)或 `…/v1//chat/completions`。
2529
+ // 探测路径早就用 normalizeBaseUrl 挡了这一手(endpoint-map.ts),发送路径
2530
+ // 漏了——症状于是固定为「连接测试通过、对话静默失败」。
2531
+ // 仅 OpenAI 分支适用:AnthropicClient 自己补 `/v1/messages`,对它剥尾巴会
2532
+ // 反造出 `/v1/v1/messages`——那是另一种错法,不在本处收口。
2533
+ baseUrl: normalizeBaseUrl(provider.baseUrl),
2534
+ apiKey: params.apiKey,
2535
+ model: params.model,
2536
+ maxTokens: params.maxTokens,
2537
+ auth: params.auth,
2538
+ thinking: provider.thinking,
2539
+ thinkingStallTimeoutMs: provider.thinkingStallTimeoutMs ?? wire?.thinkingStallTimeoutMs,
2540
+ firstByteTimeoutMs: provider.firstByteTimeoutMs,
2541
+ // Advanced provider knobs and slow-thinking override are both runtime inputs.
2542
+ requestTimeoutMs: provider.requestTimeoutMs,
2543
+ maxRetries: provider.maxRetries,
2544
+ retry: provider.retry,
2545
+ temperature: provider.temperature,
2546
+ proxy: provider.proxy,
2547
+ slowThinking: provider.slowThinking,
2548
+ thinkingBlockType: capabilities.thinkingBlockType,
2549
+ reasoningSplit: capabilities.reasoningSplit,
2550
+ thinkingBudgetField: capabilities.thinkingBudgetField,
2551
+ effortCap: capabilities.effortCap,
2552
+ effortFormat: capabilities.effortFormat,
2553
+ reasoningEffort: params.reasoningEffort,
2554
+ sessionId,
2555
+ sessionHeader: wire?.sessionHeader,
2556
+ providerName: provider.name,
2557
+ // 401/403 报错里点名 key 的环境变量,用户知道去哪检查。
2558
+ apiKeyEnv: provider.apiKeyEnv,
2559
+ // Preserved-thinking protocol family: table-driven capability, not provider
2560
+ // name — the pro spark preset declares it via capabilities override, so no
2561
+ // pro provider name leaks into open-source wire code. Distinct from the
2562
+ // deepseek-native prefix-cache strategy (GLM/longcat share the cache
2563
+ // strategy but have independent reasoning — they must NOT get this).
2564
+ preservedThinkingProtocol: capabilities.preservedThinkingProtocol ?? false,
2565
+ providerProfile: getProviderProfile(provider.name, modelContextWindow(provider, params.model)),
2566
+ wireContext: params.wireContext,
2567
+ unsupported: provider.unsupported.length > 0 ? provider.unsupported : capabilities.stripParams,
2568
+ prefixCompletion: provider.capabilities.prefixCompletion,
2569
+ useMaxCompletionTokens: wire?.useMaxCompletionTokens,
2570
+ userAgent: wire?.userAgent,
2571
+ usageCalibrationFactor: provider.usageCalibrationFactor,
2572
+ capabilities: { hasToolJsonInContentBug: capabilities.hasToolJsonInContentBug }
2573
+ });
2574
+ }
2575
+ function modelContextWindow(provider, modelId) {
2576
+ const pool = contractModels(provider);
2577
+ return pool.find((model) => model.id === modelId)?.contextWindow ?? pool[0]?.contextWindow ?? 128e3;
2578
+ }
2579
+
2580
+ export {
2581
+ fetchWithTimeout,
2582
+ REASONING_FIRST_BYTE_TIMEOUT_MS,
2583
+ SLOW_FIRST_BYTE_TIMEOUT_MS,
2584
+ FIRST_BYTE_PER_100K_MS,
2585
+ isSlowThinkingProvider,
2586
+ resolveCapabilities,
2587
+ providerIdentityHeaders,
2588
+ resolveApiKey,
2589
+ tryResolveCredentialKey,
2590
+ resolveCredentialKey,
2591
+ createProviderClient
2592
+ };