thincoder 0.12.62 → 0.12.63

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 (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
@@ -1,476 +0,0 @@
1
- /**
2
- * provider/core.mjs — LLM call core
3
- * chat / createProvider / requestWithRetry
4
- * SSE parsing → provider/sse.mjs
5
- */
6
-
7
- import { providerSpec, resolveEnableThinking } from "../config.mjs"
8
- import { proxyFetch } from "../proxy.mjs"
9
- import { escapeMessages, stripLocalMessageFields } from "../escape.mjs"
10
- import { logEvent, errText, classifyErr, headText } from "../log.mjs"
11
- import { abortError, annotateAbort, deathLine } from "../abort-provenance.mjs"
12
- import { recordChatTrace } from "../traces/trace-store.mjs"
13
- import { readSSE } from "./sse.mjs"
14
- export { readSSE } from "./sse.mjs"
15
- import {
16
- RETRYABLE_STATUS, MAX_RETRIES, MAX_CONTINUATIONS,
17
- RATE_LIMIT_BACKOFF_MS, _rateHooks,
18
- estimateRequestTokens, rateGate, recordRate,
19
- } from "./rate.mjs"
20
- // 2026-09-05 module-split:错误分类/流规则族迁 provider/errors.mjs(core.mjs 557 > 500 硬限)
21
- import { parseRetryAfter, isNonRetryableError, betaBaseURL, compileStreamRules, assertProviderModel } from "./errors.mjs"
22
- // 测试 import 面(provider-stream/stream-rules)——core 曾直接 export 这两个
23
- export { parseRetryAfter, compileStreamRules } from "./errors.mjs"
24
-
25
- // 2026-09-01:FETCH_TIMEOUT_MS 常量退役(绝对墙钟语义废除)——fetchTimeoutMs 现为每调用从 provider 读(config 归一化),见 effectiveFetchTimeoutMs。
26
-
27
- /** 可中断 sleep(会诊 #5)——retry.mjs 同用(2026-09-08 ENG-SESSION-PROVIDER-CLEANUP D2.3 去重——单实现,retry.mjs 导入)。 */
28
- export async function sleepInterruptible(ms, signal) {
29
- if (!signal) return _rateHooks.sleep(ms)
30
- if (signal.aborted) throw abortError(signal, "provider", "sleep")
31
- return new Promise((resolve, reject) => {
32
- const onAbort = () => { signal.removeEventListener("abort", onAbort); reject(abortError(signal, "provider", "sleep")) }
33
- signal.addEventListener("abort", onAbort, { once: true })
34
- _rateHooks.sleep(ms).then(
35
- () => { signal.removeEventListener("abort", onAbort); resolve() },
36
- (e) => { signal.removeEventListener("abort", onAbort); reject(e) },
37
- )
38
- })
39
- }
40
-
41
- /** Create a validated provider config object from raw config */
42
- export function createProvider(config) {
43
- if (!config?.baseURL) throw new Error("provider config: baseURL is required — configure providers in ~/.thincoder/config.json")
44
- if (!config?.apiKey) throw new Error("provider config: apiKey is required — configure it in ~/.thincoder/config.json")
45
- if (!config?.model) throw new Error("provider config: model is required — configure in ~/.thincoder/config.json")
46
- return {
47
- baseURL: config.baseURL.replace(/\/+$/, ""),
48
- apiKey: config.apiKey,
49
- model: config.model,
50
- maxTokens: config.maxTokens,
51
- temperature: config.temperature,
52
- thinking: config.thinking,
53
- reasoningEffort: config.reasoningEffort,
54
- tpm: config.tpm,
55
- rpm: config.rpm,
56
- format: config.format,
57
- chatPath: config.chatPath,
58
- proxy: config.proxy,
59
- proxyUri: config.proxyUri,
60
- }
61
- }
62
-
63
- /** Send a streaming chat completion request with automatic continuation on truncation */
64
- // 2026-09-01 根因修复:600s 绝对墙钟曾腰斩长上下文子代理(eng-coder TTFB>10min 即死)——TTFB 阶段改用
65
- // fetchTimeoutMs(默认 600s,agent.fetchTimeoutMs 可配),body 阶段 idle 超时(FETCH_BODY_IDLE_MS,无新数据才断)。
66
- const FETCH_BODY_IDLE_MS = 120_000
67
- /** §14.2 设计值:prefix 续写只保留最近 8 条非工具文本(截断点语境足够,N 以测试锁定) */
68
- const PREFIX_CONTINUATION_KEEP = 8
69
-
70
- /** 2026-09-01:响应头阶段超时(默认 600s,agent.fetchTimeoutMs 可配)——anthropic/responses transport 共用 */
71
- export function effectiveFetchTimeoutMs(provider) {
72
- return Number.isFinite(provider?.fetchTimeoutMs) && provider.fetchTimeoutMs > 0 ? provider.fetchTimeoutMs : 600_000
73
- }
74
-
75
- export async function chat(provider, opts = {}) {
76
- // LOGGING(docs/design/LOGGING.md):llm:* 事件统一在此落点——所有 chat 调用
77
- // (主回合/消化轮/compress/distill/advisor/子代理/auto-think/consult)都经本函数,
78
- // 格式分派(anthropic/google/responses)在内部——单点覆盖即 llm:* 全覆盖。
79
- // 续写/重试各自为独立 HTTP 请求——续写递归(下方 chatImpl 内)会再包一层(嵌套
80
- // llm:start/done 对——每请求一事件);重试在 requestWithRetry 内部不可见。
81
- // §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 N-TR2——权威句 D-TR1):采集点唯一=
82
- // 本函数出口——所有 chat 调用(主回合/消化轮/compress/distill/advisor/子代理/
83
- // auto-think/consult)都经本函数;续写/重试在出口已合并——reasoning 全量才完整。
84
- const logCtx = opts.logCtx ?? {}
85
- const t0 = Date.now()
86
- const pname = provider?.name ?? provider?.model ?? "unknown"
87
- logEvent("llm:start", { provider: pname, model: provider?.model ?? "", stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child })
88
- try {
89
- const result = await chatImpl(provider, opts)
90
- logEvent("llm:done", {
91
- provider: pname, model: provider?.model ?? "",
92
- ms: Date.now() - t0,
93
- stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child,
94
- head: headText(result?.content ?? "", 300, { paragraph: true }),
95
- len: String(result?.content ?? "").length,
96
- finish: result?.finishReason ?? null,
97
- tools: Array.isArray(result?.toolCalls) ? result.toolCalls.length : 0,
98
- })
99
- // §18.6 D-TR1/D-TR5:出口收集——成功路径轨迹(含 content/reasoning 全文/toolCalls)
100
- recordChatTrace(provider, opts, result, null)
101
- return result
102
- } catch (e) {
103
- logEvent("llm:error", {
104
- provider: pname, model: provider?.model ?? "",
105
- ms: Date.now() - t0,
106
- stage: logCtx.stage, turn: logCtx.turn, auto: logCtx.auto === true, child: logCtx.child,
107
- err: errText(deathLine(e, opts.signal), 200),
108
- kind: classifyErr(e, opts.signal),
109
- })
110
- // §18.6 D-TR5:失败路径也落盘——error(errText 截断 + 类别)+ finishReason:null
111
- recordChatTrace(provider, opts, null, e)
112
- throw e
113
- }
114
- }
115
-
116
- /** chat 本体(LOG(LLM) 事件包装之外——见上方 chat 包装器)。 */
117
- async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWait, signal, streamRules, firedPatterns, toolChoice, parallelToolCalls, logCtx }) {
118
- // Sanitize BEFORE format dispatch — image poisoning bricks anthropic/google sessions
119
- // the same way it bricks OpenAI-format ones (all raster-only).
120
- // providerSpec: spec with the provider-level context override (PROVIDER.md §15) — the
121
- // window/clamping logic below reads the overridden value where it matters.
122
- const spec = providerSpec(provider)
123
- messages = stripImagesForTextModel(messages, spec)
124
- // SESSION.md §9 T-S3: local-only message fields (ts/transient) never reach the wire.
125
- // Stripped BEFORE format dispatch — anthropic/responses transports pass whole message
126
- // objects through verbatim (only the OpenAI path ran escapeMessages). Copy-on-write:
127
- // history keeps the fields, the request never sees them.
128
- messages = stripLocalMessageFields(messages)
129
- const _debugBeforeLen = process.env.THIN_DEBUG_BODY ? JSON.stringify(messages).length : 0
130
-
131
- // Format dispatch: delegate to non-OpenAI transports
132
- if (provider.format === "anthropic") {
133
- const { chat: anthropicChat } = await import("./anthropic.mjs")
134
- const { normalizeTools } = await import("./anthropic.mjs")
135
- const result = await anthropicChat(provider, {
136
- messages,
137
- tools: tools?.length ? normalizeTools(tools) : null,
138
- onToken, onReasoning, onWait, signal, toolChoice,
139
- })
140
- return result
141
- }
142
- if (provider.format === "google") {
143
- const { chat: geminiChat } = await import("./google.mjs")
144
- const { normalizeTools } = await import("./google.mjs")
145
- const result = await geminiChat(provider, {
146
- messages,
147
- tools: tools?.length ? normalizeTools(tools) : null,
148
- onToken, onReasoning, onWait, signal, toolChoice,
149
- })
150
- return result
151
- }
152
- if (provider.format === "responses") {
153
- // 2026-08-31:Responses API transport(PROVIDER.md §13)——双轨链在 transport 内部
154
- // 自行管理(provider._responsesChain),agent 层零改动。
155
- // round3 #3:配对归一化必须在此分派前(压缩/中断遗留的孤儿 tool 消息发向严格服务端会 400)
156
- messages = normalizeToolPairing(messages)
157
- const { chat: responsesChat } = await import("./responses.mjs")
158
- return responsesChat(provider, {
159
- messages,
160
- tools,
161
- onToken, onReasoning, onWait, signal, toolChoice,
162
- })
163
- }
164
-
165
- messages = normalizeToolPairing(messages)
166
- // 中和服务端的非标二次转义:会话里若出现字面 "\x"/"\u"(如讨论转义、grep 到含
167
- // 转义的代码),Kimi 等会把它们当 hex escape 再解析 → "unexpected end of hex escape" 400。
168
- // 发送前统一 double 掉会形成非法转义的序列(合法 \xNN/\uNNNN 不受影响)。
169
- messages = escapeMessages(messages)
170
- if (process.env.THIN_DEBUG_BODY) {
171
- console.error(`[debug-body] escape: ${_debugBeforeLen} -> ${JSON.stringify(messages).length} chars, ${messages.length} msgs (provider=${provider.name}, model=${provider.model})`)
172
- }
173
- // Compile string-pattern rules to RegExp at call time
174
- const rules = compileStreamRules(streamRules)
175
- // F-1 (MODEL-400-FIX):请求体组装前断言 model 恒有值——见 errors.mjs assertProviderModel
176
- assertProviderModel(provider)
177
- const body = {
178
- model: provider.model,
179
- messages,
180
- stream: true,
181
- }
182
- // Skip usage stream for models that don't support it (GLM, MiniMax, Gemini)
183
- if (!spec.noUsageStream) body.stream_options = { include_usage: true }
184
- if (provider.maxTokens) body.max_tokens = provider.maxTokens
185
- if (provider.temperature != null) {
186
- let t = provider.temperature
187
- if (spec.tempRange) {
188
- t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
189
- t = Math.round(t * 100) / 100
190
- }
191
- body.temperature = t
192
- }
193
- if (provider.thinking) body.thinking = provider.thinking
194
- // reasoning_effort is a provider-native parameter — routers/proxies (model ID with "/"
195
- // prefix like kimi/kimi-k3) may misinterpret it, causing empty responses or 400s.
196
- const isRouter = provider.model.includes("/")
197
- if (provider.reasoningEffort && !isRouter && provider.format !== "anthropic" && provider.format !== "google") {
198
- if (spec.reasoningEffortEnum && !spec.reasoningEffortEnum.includes(provider.reasoningEffort)) {
199
- throw new Error(
200
- `reasoning_effort "${provider.reasoningEffort}" not supported by model "${provider.model}"; ` +
201
- `valid values: ${spec.reasoningEffortEnum.join(", ")}`
202
- )
203
- }
204
- body.reasoning_effort = provider.reasoningEffort
205
- }
206
- // enable_thinking — Bailian hybrid-thinking switch (PROVIDER.md §12): qwen3.x defaults to
207
- // thinking ON, so an explicit off must send enable_thinking:false or the server keeps thinking.
208
- // NOT gated by isRouter: the whitelist keys on model prefix + Bailian host, not the model-ID slash.
209
- const enableThinking = resolveEnableThinking(provider, spec)
210
- if (enableThinking !== undefined) body.enable_thinking = enableThinking
211
- if (tools?.length) body.tools = tools
212
- // 2026-08-31:tool_choice 能力层(透传 OpenAI 语义);
213
- // parallel_tool_calls 仅显式 true 时发送(默认不发=不改变现有行为)
214
- if (toolChoice !== undefined) body.tool_choice = toolChoice
215
- if (parallelToolCalls === true) body.parallel_tool_calls = true
216
-
217
- const estimated = estimateRequestTokens(body)
218
- await rateGate(provider, estimated, onWait, signal)
219
-
220
- const response = await requestWithRetry(provider, body, signal, onWait)
221
- const result = await readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns })
222
- recordRate(provider, estimated, result.usage)
223
-
224
- // Stream rule triggered, user interrupted, or network partial — return immediately.
225
- // 2026-08-31 会诊 #2:partial(网络错误中断但已有内容)与 interrupted 同级透传,
226
- // 不再让上层把已收内容当整轮失败重试(重试从零开始浪费已流出的成本)。
227
- if (result.ruleTriggered) return result
228
- if (result.interrupted) return result
229
- if (result.partial) return result
230
-
231
- // Retry on transient server overload (DeepSeek: insufficient_system_resource)
232
- const MAX_OVERLOAD_RETRIES = 1
233
- for (let r = 0; result.finishReason === "insufficient_system_resource" && r <= MAX_OVERLOAD_RETRIES; r++) {
234
- if (r > 0) {
235
- onWait?.({ phase: "overloaded", seconds: 3 })
236
- await sleepInterruptible(3000, signal)
237
- }
238
- const retryResponse = await requestWithRetry(provider, body, signal, onWait)
239
- const retryResult = await readSSE(retryResponse, { onToken, onReasoning })
240
- recordRate(provider, estimated, retryResult.usage)
241
- if (retryResult.finishReason !== "insufficient_system_resource") {
242
- // Merge any partial content from the failed attempt (streaming already showed it)
243
- result.content += retryResult.content
244
- result.reasoning += retryResult.reasoning ?? ""
245
- mergeRetryToolCalls(result, retryResult.toolCalls)
246
- result.finishReason = retryResult.finishReason
247
- if (retryResult.usage) result.usage = retryResult.usage
248
- break
249
- }
250
- // Retry exhausted — keep the partial result with insufficient_system_resource finish_reason
251
- }
252
-
253
- if (!spec.partialMode && !spec.prefixMode) return result
254
- for (let n = 0; result.finishReason === "length" && result.content && n < MAX_CONTINUATIONS; n++) {
255
- let continued
256
- try {
257
- continued = await chat(spec.prefixMode ? { ...provider, baseURL: betaBaseURL(provider.baseURL) } : provider, {
258
- messages: buildContinuationMessages(messages, result, spec),
259
- tools,
260
- onToken,
261
- onReasoning,
262
- onWait,
263
- signal,
264
- // §18.6:续写是同一逻辑调用的子请求——logCtx 原样透传(元数据与门控
265
- // traces.enabled 对续写调用同样生效,不在出口静默越过开关)
266
- // fix round1(D-TR1):续写子请求标记 isContinuation:true(T-TR14——true =
267
- // 该调用是续写链的一环;外层新调用 false)——分析"纠结"时区分续写/重试链。
268
- logCtx: { ...logCtx, isContinuation: true },
269
- })
270
- } catch (error) {
271
- // §14.3 失败可见性:续写失败注入 _warnings(agent 机读线可见)不整轮飞出;AbortError 用户中断透传
272
- if (error?.name === "AbortError") throw error
273
- result._warnings ??= []
274
- result._warnings.push({ name: "continuation-failed", message: `output continuation failed: ${error.message}` })
275
- break
276
- }
277
- result.content += continued.content
278
- result.reasoning += continued.reasoning ?? ""
279
- mergeRetryToolCalls(result, continued.toolCalls)
280
- result.finishReason = continued.finishReason
281
- if (continued.usage) {
282
- const sum = (k) => (result.usage?.[k] ?? 0) + (continued.usage[k] ?? 0)
283
- result.usage = {
284
- prompt_tokens: sum("prompt_tokens"),
285
- completion_tokens: sum("completion_tokens"),
286
- total_tokens: sum("total_tokens"),
287
- prompt_cache_hit_tokens: sum("prompt_cache_hit_tokens"),
288
- prompt_cache_miss_tokens: sum("prompt_cache_miss_tokens"),
289
- }
290
- }
291
- }
292
- return result
293
- }
294
-
295
- /** 续写消息构造(§14.3):prefix 精简历史(§14.2——deepseek /beta 网关对含工具链历史必 400,真机矩阵);partial 保持现状 */
296
- export function buildContinuationMessages(messages, result, spec) {
297
- const tail = (extra) => ({ role: "assistant", content: result.content, ...extra, ...(result.reasoning ? { reasoning_content: result.reasoning } : {}) })
298
- if (!spec.prefixMode) return [...messages, tail({ partial: true })]
299
- const slim = messages.filter((m) => m.role !== "tool" && !(m.role === "assistant" && m.tool_calls?.length))
300
- return [...slim.filter((m) => m.role === "system"), ...slim.filter((m) => m.role !== "system").slice(-PREFIX_CONTINUATION_KEEP), tail({ prefix: true })]
301
- }
302
-
303
- /**
304
- * Replace image parts with text placeholders when they would 400 the request:
305
- * - the model has no vision support at all (history may carry image_url parts from a
306
- * session resumed after switching from a vision model — text-only APIs like DeepSeek
307
- * reject the ENTIRE request, bricking the conversation);
308
- * - the model IS vision-capable but the data URL is not a raster format it can ingest
309
- * (Kimi/Anthropic/OpenAI/Gemini are all raster-only — Kimi 400s "unsupported image
310
- * format" on EVERY subsequent request once an svg/bmp part sits in history).
311
- * Sanitize at send time — history itself is left untouched, so switching back to a
312
- * capable model/format restores the images. Non-data-URL image refs (http) pass through.
313
- */
314
- // Pre-send payload normalization lives in normalize.mjs (2026-08-31 extract,
315
- // TODO #2); re-exported so provider/index.mjs and tool-pairing.test.mjs keep
316
- // their import paths.
317
- import { stripImagesForTextModel, normalizeToolPairing } from "./normalize.mjs"
318
- export { stripImagesForTextModel, normalizeToolPairing }
319
- /** Merge tool calls from a retry/continuation into the accumulated result.
320
- * 2026-08-31 会诊 #7/#17:readSSE 输出的 tc 已 finalize(无 index 字段),
321
- * 原实现恒 append(重试里 provider 重发完整 tc → tool 名 "get_weatherget_weather"、
322
- * arguments 重复)。改按 id 定位已有槽位、无 id 才追加;name 只设一次。 */
323
- function mergeRetryToolCalls(result, toolCalls) {
324
- for (const tc of toolCalls ?? []) {
325
- if (!tc) continue
326
- let s
327
- if (tc.id) {
328
- s = result.toolCalls.find((x) => x && x.id === tc.id)
329
- }
330
- if (!s) {
331
- // 无 id(synthetic call_N 在重试间不稳定)或未命中:按 name 找同 slot(重试语义
332
- // 是"同一批工具调用重新执行",同名合并最稳);仍找不到才追加。
333
- s = tc.name ? result.toolCalls.find((x) => x && x.name === tc.name) : undefined
334
- }
335
- if (!s) {
336
- s = { id: "", name: "", arguments: "" }
337
- result.toolCalls.push(s)
338
- }
339
- if (tc.id && !s.id) s.id = tc.id
340
- if (tc.name && !s.name) s.name = tc.name
341
- s.arguments += tc.arguments ?? ""
342
- }
343
- }
344
-
345
- // listModels 已迁 provider/list-models.mjs(PROVIDER.md §16 M1——按 format 分派):2026-09-10
346
- // 模型选择面重构——原 OpenAI-only 实现在此,迁出并扩 anthropic / google 两分支;
347
- // provider/index.mjs re-export 改指新文件——调用点零改。
348
-
349
- async function requestWithRetry(provider, body, signal, onWait) {
350
- // THIN_DEBUG_BODY=1:发送前诊断——复现网关侧 "unexpected end of hex escape" 400 时
351
- // 定位真实载荷里的毒序列(2026-08-31 slot 3 deepseek-v4-flash)。模拟网关最宽松的
352
- // 爆炸条件:任何字面 "\u"/"\x" 后不足位(不看前置反斜杠)。
353
- if (process.env.THIN_DEBUG_BODY) {
354
- try {
355
- const msgs = body?.messages ?? []
356
- const raw = JSON.stringify(body)
357
- const hits = []
358
- for (let i = 0; i < msgs.length; i++) {
359
- const m = msgs[i] ?? {}
360
- const fields = []
361
- if (typeof m.content === "string") fields.push(["content", m.content])
362
- else if (Array.isArray(m.content)) m.content.forEach((p, pi) => { if (p && typeof p.text === "string") fields.push([`content[${pi}]`, p.text]) })
363
- if (typeof m.reasoning_content === "string") fields.push(["reasoning_content", m.reasoning_content])
364
- if (Array.isArray(m.tool_calls)) m.tool_calls.forEach((tc, ti) => { if (tc && typeof tc.arguments === "string") fields.push([`tool_calls[${ti}].arguments`, tc.arguments]) })
365
- if (typeof m.name === "string") fields.push(["name", m.name])
366
- for (const [f, t] of fields) {
367
- const re = /\\[xu]/g
368
- let mm
369
- while ((mm = re.exec(t))) {
370
- const c = t[mm.index + 1]
371
- const need = c === "u" ? 4 : 2
372
- const after = t.slice(mm.index + 2, mm.index + 2 + need)
373
- if (!new RegExp(`^[0-9a-fA-F]{${need}}$`).test(after)) {
374
- hits.push({ i, role: m.role, field: f, ctx: t.slice(Math.max(0, mm.index - 40), mm.index + 12) })
375
- }
376
- }
377
- }
378
- }
379
- console.error(`[debug-body] messages=${msgs.length} bodyLen=${raw.length} suspicious=${hits.length}`)
380
- for (const h of hits.slice(0, 20)) console.error("[debug-body] hit", JSON.stringify(h))
381
- if (!hits.length && msgs[1151]) {
382
- console.error("[debug-body] no suspicious hit; messages[1151] =", JSON.stringify({ role: msgs[1151].role, contentLen: msgs[1151].content?.length, contentHead: String(msgs[1151].content).slice(0, 150) }))
383
- }
384
- } catch (e) {
385
- console.error("[debug-body] diag failed:", e.message)
386
- }
387
- }
388
- let lastError
389
- let lastStatus = 0
390
- let lastWas429 = false
391
- let rateLimitHits = 0
392
- const totalAttempts = MAX_RETRIES + 1
393
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
394
- if (attempt > 0 && !lastWas429) await sleepInterruptible(2 ** (attempt - 1) * 1000, signal)
395
- lastWas429 = false
396
-
397
- let response
398
- try {
399
- const url = `${provider.baseURL}${provider.chatPath ?? "/chat/completions"}`
400
- const opts = {
401
- method: "POST",
402
- headers: {
403
- ...(provider.headers ?? {}), // custom per-provider headers (desktop proposal ④: X-Device-Id etc.)
404
- "Content-Type": "application/json",
405
- Authorization: `Bearer ${provider.apiKey}`,
406
- },
407
- body: JSON.stringify(body),
408
- // 2026-09-01 根因修复:原 600s 绝对墙钟会腰斩长上下文子代理(TTFB/首 token >10min 即死)。
409
- // 拆分语义:响应头阶段仍用 fetchTimeoutMs(600s,覆盖网关排队);body 阶段由读侧 idle 超时管
410
- // (sse.mjs readIdleMs——无新数据才断)。signal 只保留用户取消链,不再叠加绝对墙钟。
411
- signal,
412
- // 2026-08-31 会诊 #4:代理路径响应头超时对齐直连语义(原 15s 与直连 600s 割裂,
413
- // DeepSeek 排队 TTFB>15s 即误报)— 仅 _ 前缀内部字段,proxyFetch 消费
414
- _headerTimeoutMs: effectiveFetchTimeoutMs(provider),
415
- _bodyIdleMs: FETCH_BODY_IDLE_MS,
416
- }
417
- response = provider.proxyUri
418
- ? await proxyFetch(url, opts, provider.proxyUri)
419
- : await fetch(url, opts)
420
- } catch (error) {
421
- // §20.3 站点 #2(第 24 批):fetch 拒否面补标来源(undici 拒否的 AbortError 现场无 reason)
422
- if (error.name === "AbortError") throw annotateAbort(error, signal, "provider", "request")
423
- lastError = error
424
- continue
425
- }
426
-
427
- if (response.ok) return response
428
-
429
- const text = await response.text().catch(() => "")
430
- let message = `LLM API error ${response.status}: ${text}`
431
- // 401 双平台提示 + 诊断回显(2026-08-31 会诊 #15):
432
- // Kimi 双平台 key 不互通的提示保留;通用加 baseURL host + key 前 6 位掩码,
433
- // 帮用户快速分辨"配错平台还是配错账号"。
434
- if (response.status === 401 || response.status === 403) {
435
- const key = String(provider.apiKey ?? "").trim()
436
- const base = String(provider.baseURL ?? "").toLowerCase()
437
- const kimiCodeKey = /^sk-kimi-/i.test(key)
438
- const kimiCodeUrl = base.includes("api.kimi.com")
439
- if (kimiCodeKey || kimiCodeUrl) {
440
- message += " — tip: Kimi has two separate platforms with NON-interchangeable API keys: Moonshot (api.moonshot.cn/v1, sk-...) and Kimi For Coding (api.kimi.com/coding/v1, sk-kimi-...). Your key or baseURL looks mismatched — check which platform issued it."
441
- }
442
- const host = (() => { try { return new URL(provider.baseURL).host } catch { return provider.baseURL ?? "(unknown)" } })()
443
- const masked = key.length > 8 ? key.slice(0, 6) + "…" + key.slice(-4) : (key ? key.slice(0, 4) + "…" : "(empty)")
444
- message += ` [auth diag: baseURL=${host} key=${masked} status=${response.status}]`
445
- }
446
- lastStatus = response.status
447
- if (isNonRetryableError(response.status, text)) throw new Error(message)
448
- if (response.status === 429) {
449
- const waitMs = parseRetryAfter(response.headers.get("retry-after"), rateLimitHits)
450
- rateLimitHits++
451
- lastError = new Error(message)
452
- lastWas429 = true
453
- if (attempt < MAX_RETRIES) {
454
- onWait?.({ phase: "retry", seconds: Math.ceil(waitMs / 1000) })
455
- await sleepInterruptible(waitMs, signal)
456
- }
457
- continue
458
- }
459
- if (RETRYABLE_STATUS.has(response.status)) {
460
- lastError = new Error(message)
461
- continue
462
- }
463
- throw new Error(message)
464
- }
465
- // All retries exhausted — build a descriptive error
466
- const verb = lastWas429 ? "Rate limit not resolved"
467
- : lastStatus >= 500 ? "Server error persisted"
468
- : lastStatus > 0 ? "Request failed"
469
- : "Network error"
470
- // 会诊 #8:undici "fetch failed" 真因(ENOTFOUND/TLS/DNS/代理)藏在 error.cause —
471
- // 拼进去,全链路同一文案不再掩盖根因
472
- const causeText = lastError?.cause
473
- ? ` (${lastError.cause.code ?? lastError.cause.message ?? String(lastError.cause)})`
474
- : ""
475
- throw new Error(`${verb} after ${totalAttempts} attempts${lastStatus ? ` (${lastStatus})` : ""}: ${lastError?.message ?? "unknown"}${causeText}`)
476
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * provider/errors.mjs — 错误分类与流规则编译族(2026-09-05 module-split:core.mjs
3
- * 557 > 500 硬限——parseRetryAfter/isNonRetryableError/betaBaseURL/compileStreamRules
4
- * verbatim 迁入,语义零变;core.mjs import 回(chat 调用点零改)。
5
- * 注:retry.mjs(anthropic/google/responses 通道)自 2026-09-08 起导入本文件的
6
- * parseRetryAfter/isNonRetryableError(ENG-SESSION-PROVIDER-CLEANUP D2.2/D2.4 去重——
7
- * 单实现;早先的"循环依赖回避"复制已随依赖方向实测消解)。
8
- */
9
-
10
- import { RETRYABLE_STATUS, RATE_LIMIT_BACKOFF_MS } from "./rate.mjs"
11
-
12
- /** Parse Retry-After: 秒数 or HTTP-date;上限 300s(会诊 #11)— 异常头不得让 CLI 睡数小时。
13
- * header 缺失/非法时退回指数退避表(rateLimitHits 计数取档)。 */
14
- export function parseRetryAfter(header, rateLimitHits = 0) {
15
- const fallback = RATE_LIMIT_BACKOFF_MS[Math.min(rateLimitHits, RATE_LIMIT_BACKOFF_MS.length - 1)]
16
- if (header == null) return fallback
17
- let waitMs = 0
18
- const numeric = Number(header.trim())
19
- if (Number.isFinite(numeric) && numeric >= 0) waitMs = numeric * 1000
20
- else {
21
- const date = Date.parse(header.trim())
22
- if (Number.isFinite(date)) waitMs = Math.max(0, date - Date.now())
23
- }
24
- if (waitMs <= 0) return fallback
25
- return Math.min(waitMs, 300_000)
26
- }
27
-
28
- /**
29
- * Detect errors that should NOT be retried — quota, billing, auth, invalid params.
30
- * Different providers use wildly different error formats. Check body text for known patterns.
31
- */
32
- export function isNonRetryableError(status, text) {
33
- // Auth errors: never retry
34
- if (status === 401 || status === 403) return true
35
- // 400-level non-429: usually invalid params
36
- if (status >= 400 && status < 500 && status !== 429 && !RETRYABLE_STATUS.has(status)) return true
37
- // For 429, check if it's actually a billing/quota error (not rate limit)
38
- if (status === 429) {
39
- const lower = text.toLowerCase()
40
- // Chinese providers often return 429 for billing issues
41
- if (lower.includes("余额不足") || lower.includes("余额") || lower.includes("充值")) return true
42
- if (lower.includes("insufficient") && (lower.includes("balance") || lower.includes("quota") || lower.includes("credit"))) return true
43
- if (lower.includes("quota") && (lower.includes("exceeded") || lower.includes("insufficient"))) return true
44
- // Standard OpenAI billing error (error.type === "insufficient_quota" or similar)
45
- try {
46
- const j = JSON.parse(text)
47
- const errType = j?.error?.type || ""
48
- if (typeof errType === "string" && (errType.includes("quota") || errType.includes("billing") || errType.includes("insufficient") || errType.includes("balance"))) return true
49
- const errCode = j?.error?.code || ""
50
- if (typeof errCode === "string" && (errCode === "1113" || errCode === "1114")) return true // GLM billing codes
51
- } catch {}
52
- }
53
- return false
54
- }
55
-
56
- export function betaBaseURL(baseURL) {
57
- // DeepSeek prefix continuation uses /beta endpoint; only handle /v1 suffix, append /beta when /v1 is missing
58
- if (/\/v1$/.test(baseURL)) return baseURL.replace(/\/v1$/, "/beta")
59
- return baseURL.endsWith("/") ? baseURL + "beta" : baseURL + "/beta"
60
- }
61
-
62
- /**
63
- * Compile stream rules from config format (string patterns) to executable RegExp objects.
64
- * Rules format: { pattern: "regex source", message: "reminder text", action: "abort"|"warn" }
65
- */
66
- export function compileStreamRules(rules) {
67
- if (!rules?.length) return null
68
- return rules.map((r) => {
69
- try {
70
- return { ...r, _regex: new RegExp(r.pattern, r.flags ?? "") }
71
- } catch {
72
- // Invalid regex — skip silently so one bad rule doesn't break the whole pipeline
73
- return null
74
- }
75
- }).filter(Boolean)
76
- }
77
-
78
- /**
79
- * Provider-level pre-flight error (MODEL-400-FIX F-1 — 请求体组装前断言): carries the
80
- * provider identity so a fail-fast throw is readable ("which provider + what to fix")
81
- * instead of a wire-time serde 400 or a bare message without context.
82
- */
83
- export class ProviderError extends Error {
84
- constructor(provider, message) {
85
- super(`provider "${provider?.name ?? provider?.model ?? "unknown"}": ${message}`)
86
- this.name = "ProviderError"
87
- }
88
- }
89
-
90
- /**
91
- * F-1 (MODEL-400-FIX) 根因兜底——请求体组装前断言:渠道裸克隆(`{...渠道}`——渠道只带
92
- * 默认单值 `model`,克隆链未重派生 `.model` 时)provider.model 为 undefined/null——
93
- * JSON.stringify 会丢 undefined 键 → 无 model 请求 → serde 400。
94
- * fail-fast 报可读错误(带 provider 名 + 修复线索),不发病体。core.mjs chatImpl openai body 组装
95
- * 前调用(单行——core.mjs 500 行硬限)。
96
- */
97
- export function assertProviderModel(provider) {
98
- if (!provider.model) {
99
- throw new ProviderError(provider, "model is undefined — provider cloned without model re-derivation (set providers[].model — the channel default model)")
100
- }
101
- }