thincoder 0.12.61 → 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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  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 -371
  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 +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  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-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -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
- }