@thincoder/core 0.9.4 → 0.10.1

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 (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
package/provider/core.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { providerSpec, resolveEnableThinking } from "../config.mjs"
8
+ import { resolveProviderSecrets } from "../env-ref.mjs"
8
9
  import { proxyFetch } from "../proxy.mjs"
9
10
  import { escapeMessages, stripLocalMessageFields } from "../escape.mjs"
10
11
  import { logEvent, errText, classifyErr, headText } from "../log.mjs"
@@ -78,7 +79,7 @@ export async function chat(provider, opts = {}) {
78
79
  // 格式分派(anthropic/google/responses)在内部——单点覆盖即 llm:* 全覆盖。
79
80
  // 续写/重试各自为独立 HTTP 请求——续写递归(下方 chatImpl 内)会再包一层(嵌套
80
81
  // llm:start/done 对——每请求一事件);重试在 requestWithRetry 内部不可见。
81
- // §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 N-TR2——权威句 D-TR1):采集点唯一=
82
+ // 完整轨迹存档(TRACES.md §6.1——权威句 D-TR1):采集点唯一=
82
83
  // 本函数出口——所有 chat 调用(主回合/消化轮/compress/distill/advisor/子代理/
83
84
  // auto-think/consult)都经本函数;续写/重试在出口已合并——reasoning 全量才完整。
84
85
  const logCtx = opts.logCtx ?? {}
@@ -96,7 +97,7 @@ export async function chat(provider, opts = {}) {
96
97
  finish: result?.finishReason ?? null,
97
98
  tools: Array.isArray(result?.toolCalls) ? result.toolCalls.length : 0,
98
99
  })
99
- // §18.6 D-TR1/D-TR5:出口收集——成功路径轨迹(含 content/reasoning 全文/toolCalls)
100
+ // TRACES.md §6.1 D-TR1/D-TR5:出口收集——成功路径轨迹(含 content/reasoning 全文/toolCalls)
100
101
  recordChatTrace(provider, opts, result, null)
101
102
  return result
102
103
  } catch (e) {
@@ -107,7 +108,7 @@ export async function chat(provider, opts = {}) {
107
108
  err: errText(deathLine(e, opts.signal), 200),
108
109
  kind: classifyErr(e, opts.signal),
109
110
  })
110
- // §18.6 D-TR5:失败路径也落盘——error(errText 截断 + 类别)+ finishReason:null
111
+ // TRACES.md §6.1 D-TR5:失败路径也落盘——error(errText 截断 + 类别)+ finishReason:null
111
112
  recordChatTrace(provider, opts, null, e)
112
113
  throw e
113
114
  }
@@ -115,6 +116,10 @@ export async function chat(provider, opts = {}) {
115
116
 
116
117
  /** chat 本体(LOG(LLM) 事件包装之外——见上方 chat 包装器)。 */
117
118
  async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWait, signal, streamRules, firedPatterns, toolChoice, parallelToolCalls, logCtx }) {
119
+ // #57(provider-config-family 批):凭据值位 `${env:VAR}` 消费侧解析——单源 `../env-ref.mjs`
120
+ // (CONFIG.md §6.3)。格式分派之前:四 transport ∕ 续写 ∕ advisor ∕ 子代理 ∕ 压缩同享;
121
+ // 未设 ∕ 空串 ∕ 畸形 ⇒ 此处抛错(点名变量,无回退)。
122
+ provider = resolveProviderSecrets(provider)
118
123
  // Sanitize BEFORE format dispatch — image poisoning bricks anthropic/google sessions
119
124
  // the same way it bricks OpenAI-format ones (all raster-only).
120
125
  // providerSpec: spec with the provider-level context override (PROVIDER.md §6.15) — the
@@ -126,7 +131,6 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
126
131
  // objects through verbatim (only the OpenAI path ran escapeMessages). Copy-on-write:
127
132
  // history keeps the fields, the request never sees them.
128
133
  messages = stripLocalMessageFields(messages)
129
- const _debugBeforeLen = process.env.THIN_DEBUG_BODY ? JSON.stringify(messages).length : 0
130
134
 
131
135
  // Format dispatch: delegate to non-OpenAI transports
132
136
  if (provider.format === "anthropic") {
@@ -167,9 +171,6 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
167
171
  // 转义的代码),Kimi 等会把它们当 hex escape 再解析 → "unexpected end of hex escape" 400。
168
172
  // 发送前统一 double 掉会形成非法转义的序列(合法 \xNN/\uNNNN 不受影响)。
169
173
  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
174
  // Compile string-pattern rules to RegExp at call time
174
175
  const rules = compileStreamRules(streamRules)
175
176
  // F-1 (MODEL-400-FIX):请求体组装前断言 model 恒有值——见 errors.mjs assertProviderModel
@@ -179,7 +180,7 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
179
180
  messages,
180
181
  stream: true,
181
182
  }
182
- // Skip usage stream for models that don't support it (GLM, MiniMax, Gemini)
183
+ // 保守抑制(非能力断言):带 `noUsageStream` 的行 = 核不主动发 stream_options.include_usage(glm 族已实测带该参数全 200);逐族等级 = MODEL-SPECS.md §15.2
183
184
  if (!spec.noUsageStream) body.stream_options = { include_usage: true }
184
185
  if (provider.maxTokens) body.max_tokens = provider.maxTokens
185
186
  if (provider.temperature != null) {
@@ -276,7 +277,7 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
276
277
  onReasoning,
277
278
  onWait,
278
279
  signal,
279
- // §18.6:续写是同一逻辑调用的子请求——logCtx 原样透传(元数据与门控
280
+ // TRACES.md §6.1:续写是同一逻辑调用的子请求——logCtx 原样透传(元数据与门控
280
281
  // traces.enabled 对续写调用同样生效,不在出口静默越过开关)
281
282
  // fix round1(D-TR1):续写子请求标记 isContinuation:true(T-TR14——true =
282
283
  // 该调用是续写链的一环;外层新调用 false)——分析"纠结"时区分续写/重试链。
@@ -362,44 +363,6 @@ function mergeRetryToolCalls(result, toolCalls) {
362
363
  // provider/index.mjs re-export 改指新文件——调用点零改。
363
364
 
364
365
  async function requestWithRetry(provider, body, signal, onWait) {
365
- // THIN_DEBUG_BODY=1:发送前诊断——复现网关侧 "unexpected end of hex escape" 400 时
366
- // 定位真实载荷里的毒序列(2026-08-31 slot 3 deepseek-v4-flash)。模拟网关最宽松的
367
- // 爆炸条件:任何字面 "\u"/"\x" 后不足位(不看前置反斜杠)。
368
- if (process.env.THIN_DEBUG_BODY) {
369
- try {
370
- const msgs = body?.messages ?? []
371
- const raw = JSON.stringify(body)
372
- const hits = []
373
- for (let i = 0; i < msgs.length; i++) {
374
- const m = msgs[i] ?? {}
375
- const fields = []
376
- if (typeof m.content === "string") fields.push(["content", m.content])
377
- else if (Array.isArray(m.content)) m.content.forEach((p, pi) => { if (p && typeof p.text === "string") fields.push([`content[${pi}]`, p.text]) })
378
- if (typeof m.reasoning_content === "string") fields.push(["reasoning_content", m.reasoning_content])
379
- 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]) })
380
- if (typeof m.name === "string") fields.push(["name", m.name])
381
- for (const [f, t] of fields) {
382
- const re = /\\[xu]/g
383
- let mm
384
- while ((mm = re.exec(t))) {
385
- const c = t[mm.index + 1]
386
- const need = c === "u" ? 4 : 2
387
- const after = t.slice(mm.index + 2, mm.index + 2 + need)
388
- if (!new RegExp(`^[0-9a-fA-F]{${need}}$`).test(after)) {
389
- hits.push({ i, role: m.role, field: f, ctx: t.slice(Math.max(0, mm.index - 40), mm.index + 12) })
390
- }
391
- }
392
- }
393
- }
394
- console.error(`[debug-body] messages=${msgs.length} bodyLen=${raw.length} suspicious=${hits.length}`)
395
- for (const h of hits.slice(0, 20)) console.error("[debug-body] hit", JSON.stringify(h))
396
- if (!hits.length && msgs[1151]) {
397
- 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) }))
398
- }
399
- } catch (e) {
400
- console.error("[debug-body] diag failed:", e.message)
401
- }
402
- }
403
366
  let lastError
404
367
  let lastStatus = 0
405
368
  let lastWas429 = false
@@ -433,7 +396,7 @@ async function requestWithRetry(provider, body, signal, onWait) {
433
396
  ? await proxyFetch(url, opts, provider.proxyUri)
434
397
  : await fetch(url, opts)
435
398
  } catch (error) {
436
- // §20.3 站点 #2(第 24 批):fetch 拒否面补标来源(undici 拒否的 AbortError 现场无 reason)
399
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #2(第 24 批):fetch 拒否面补标来源(undici 拒否的 AbortError 现场无 reason)
437
400
  if (error.name === "AbortError") throw annotateAbort(error, signal, "provider", "request")
438
401
  lastError = error
439
402
  continue
@@ -14,6 +14,7 @@
14
14
  * 解析保持防御性(字段缺失即跳过该项)。候选不过滤非对话模型(embedding 等——§16.6 #14)。
15
15
  */
16
16
  import { proxyFetch } from "../proxy.mjs"
17
+ import { resolveProviderSecrets } from "../env-ref.mjs"
17
18
 
18
19
  const LIST_TIMEOUT_MS = 15_000
19
20
  /** 翻页上限(cursor loop 防死循环——任一分页失败即整体抛出,不部分返回)。 */
@@ -94,6 +95,8 @@ async function listGoogle(provider, signal) {
94
95
 
95
96
  /** List available model IDs from the provider's /models endpoint (format 分派——M1)。 */
96
97
  export async function listModels(provider, { signal } = {}) {
98
+ // #57(provider-config-family 批):清单拉取入口凭据值位解析(CONFIG.md §6.3)——三 format 分支共享。
99
+ provider = resolveProviderSecrets(provider)
97
100
  const base = String(provider?.baseURL ?? "").replace(/\/+$/, "")
98
101
  if (!base) throw new Error("GET /models failed: baseURL is missing") // §2.5 #115 并入(VSC 明确报错)
99
102
  const format = provider?.format
@@ -0,0 +1,237 @@
1
+ /**
2
+ * provider/responses-request.mjs — Responses **请求构造面**出档(2026-09-29 structure-split-2 · 台账 #620):自
3
+ * `provider/responses.mjs` 全量迁出〔原 `:22-245`〕——host 白名单判定(`isStatefulHost` ∕ `isStoreRequiredHost` ∕
4
+ * `isNonStatefulHost`)· `chainKey` · `builtinToolsFor` · `toItems` / `toTools` / `normalizeUsage` · `buildBody`;
5
+ * **结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-B)。
6
+ *
7
+ * 缝 = 同名再出口:宿主 `provider/responses.mjs` 转口 `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名
8
+ * 既有导出(消费者 import 面零改);`normalizeUsage` 被宿主 `parseStream` 消费 ⇒ 随迁导出(宿主 import)。
9
+ * 零环:本档不引宿主(方向单行);档外依赖 = `../config.mjs`(`specForModel` ∥ `isBailianHost`)。
10
+ * 链语义(双轨 ∕ 白名单 ∕ store 必开 ∥ 链重置)详述见宿主档头;本档 = 请求侧构造 ∥ 链状态机决策。
11
+ */
12
+ import { specForModel, isBailianHost } from "../config.mjs"
13
+
14
+ /** 白名单:已实证 previous_response_id 的官方端(2026-08-31 真机验证:
15
+ * 百炼 store:true 全链路 ✅;GLM(open.bigmodel.cn/api/v1)store:true 全链路 ✅)。 */
16
+ function isStatefulHost(baseURL) {
17
+ try {
18
+ const host = new URL(baseURL).hostname
19
+ return /(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ /** store 必开 host(链保留依赖 store:true——真机:百炼 store:false → 链 400;GLM 同)。
26
+ * OpenAI 官方 store:false 链仍可用,不在内。 */
27
+ export function isStoreRequiredHost(baseURL) {
28
+ try {
29
+ const host = new URL(baseURL).hostname
30
+ return isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /** 灰名单:格式完整但链未证实/不支持——显式全量 + 一次性 warning(不靠服务端报错)。
37
+ * 2026-08-31 真机后仅剩 DeepSeek(官方明确 previous_response_id 不支持且参数静默忽略)。 */
38
+ function isNonStatefulHost(baseURL) {
39
+ try {
40
+ const host = new URL(baseURL).hostname
41
+ return /(^|\.)deepseek\.com$/.test(host)
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /** 链 key:system 部分 + 最后一条 user 消息(turn 内不变、跨 turn 变、压缩后变)。 */
48
+ function chainKey(messages) {
49
+ let sig = "s:"
50
+ let lastUser = ""
51
+ for (const m of messages ?? []) {
52
+ if (m.role === "system") sig += (typeof m.content === "string" ? m.content : "") + "\u0001"
53
+ else if (m.role === "user") lastUser = typeof m.content === "string" ? m.content : ""
54
+ }
55
+ return sig + "\u0002u:" + lastUser
56
+ }
57
+
58
+ /** 内置工具声明(2026-08-31 用户拍板"内置工具还是要用",一期 web_search)。
59
+ * 按 host 映射默认集;provider.builtinTools === false 关闭、数组显式覆盖。
60
+ * 注意:内置工具由**服务端执行**——绕过我们的工具权限门/审计(产品决策,用户拍板)。 */
61
+ export function builtinToolsFor(baseURL, providerBuiltin) {
62
+ if (providerBuiltin === false) return []
63
+ if (Array.isArray(providerBuiltin)) return providerBuiltin
64
+ try {
65
+ const host = new URL(baseURL).hostname
66
+ if (/(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)deepseek\.com$/.test(host)) {
67
+ return [{ type: "web_search" }]
68
+ }
69
+ } catch { /* fallthrough */ }
70
+ return []
71
+ }
72
+
73
+ /** OpenAI Chat 消息 → Responses input items。system 提升为 instructions(不进 input)。
74
+ * 内置工具结果(web_search_call 本地化 tool 消息)→ 原样 web_search_call item 回传。 */
75
+ function toItems(messages, { instructions } = {}) {
76
+ const items = []
77
+ for (const m of messages ?? []) {
78
+ if (m.role === "system") continue
79
+ if (typeof m.tool_call_id === "string" && m.tool_call_id.startsWith("web_search_call_") && typeof m.content === "string") {
80
+ // 内置工具结果本地化消息 → 原样回传(DeepSeek 官方:web_search_call 原样回传即可,
81
+ // 服务端自动恢复搜索结果)。id 用 content 里的原始服务端 id(msg_xxx),前缀只是本地锚点。
82
+ let query = ""
83
+ let srcs = []
84
+ let wsId = m.tool_call_id.slice("web_search_call_".length)
85
+ try {
86
+ const parsed = JSON.parse(m.content)
87
+ query = parsed.query ?? ""
88
+ srcs = parsed.sources ?? []
89
+ if (parsed.id) wsId = parsed.id
90
+ } catch { /* content 非 JSON(纯展示)→ query 缺省 */ }
91
+ items.push({ type: "web_search_call", id: wsId, status: "completed", action: { query, type: "search", sources: srcs } })
92
+ continue
93
+ }
94
+ if (m.role === "user") {
95
+ const content = m.content
96
+ if (Array.isArray(content)) {
97
+ items.push({
98
+ role: "user",
99
+ content: content
100
+ .map((p) => (p?.type === "image_url"
101
+ ? { type: "input_image", image_url: p.image_url?.url }
102
+ : p?.type === "text" || typeof p === "string"
103
+ ? { type: "input_text", text: typeof p === "string" ? p : p.text }
104
+ : null))
105
+ .filter(Boolean),
106
+ })
107
+ } else {
108
+ items.push({ role: "user", content: [{ type: "input_text", text: String(content ?? "") }] })
109
+ }
110
+ } else if (m.role === "assistant") {
111
+ const tcList = m.tool_calls ?? []
112
+ items.push({ role: "assistant", content: [{ type: "output_text", text: String(m.content ?? "") }] })
113
+ for (const tc of tcList) {
114
+ items.push({
115
+ type: "function_call",
116
+ call_id: tc.id ?? "",
117
+ name: tc.function?.name ?? "",
118
+ arguments: tc.function?.arguments ?? "{}",
119
+ })
120
+ }
121
+ } else if (m.role === "tool" || m.role === "function") {
122
+ items.push({
123
+ type: "function_call_output",
124
+ call_id: m.tool_call_id ?? m.name ?? "",
125
+ output: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
126
+ })
127
+ }
128
+ }
129
+ return { items, instructions: instructions ?? (messages ?? []).find((m) => m.role === "system")?.content ?? "" }
130
+ }
131
+
132
+ /** OpenAI 工具 schema → Responses 扁平 tools。 */
133
+ function toTools(tools) {
134
+ return (tools ?? []).map((t) => ({
135
+ type: "function",
136
+ name: t.function?.name ?? t.name,
137
+ description: t.function?.description ?? t.description,
138
+ parameters: t.function?.parameters ?? t.parameters ?? { type: "object", properties: {} },
139
+ }))
140
+ }
141
+
142
+ /** Responses usage → 内部 cache 字段形状。 */
143
+ export function normalizeUsage(usage) {
144
+ if (!usage) return null
145
+ return {
146
+ prompt_tokens: usage.input_tokens ?? 0,
147
+ completion_tokens: usage.output_tokens ?? 0,
148
+ total_tokens: usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
149
+ prompt_cache_hit_tokens: usage.input_tokens_details?.cached_tokens ?? 0,
150
+ prompt_cache_miss_tokens: Math.max(0, (usage.input_tokens ?? 0) - (usage.input_tokens_details?.cached_tokens ?? 0)),
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 组装请求体 + 链状态机决策。
156
+ * @returns {{ body, previousResponseId, warnings, newChain: {id,key}|null }}
157
+ */
158
+ export function buildBody(provider, messages, tools, opts = {}) {
159
+ const { instructions: extracted, items } = toItems(messages)
160
+ const toolsFlat = tools?.length ? toTools(tools) : undefined
161
+ const spec = specForModel(provider.model)
162
+ const warnings = []
163
+ const wantStateful = provider.stateful !== false && opts.stateful !== false
164
+ const hostStateful = isStatefulHost(provider.baseURL)
165
+ const hostNonStateful = isNonStatefulHost(provider.baseURL)
166
+
167
+ let chain = provider._responsesChain ?? null
168
+ const key = chainKey(messages)
169
+
170
+ if (!wantStateful) {
171
+ chain = null // stateful:false 显式覆盖:残留链(同 session 开过)必须作废
172
+ } else if (hostNonStateful && !opts.forceStateful) {
173
+ // 灰名单:链不支持/未证实(DeepSeek 静默忽略 → 无声丢上下文)——显式全量 + 一次警告
174
+ if (wantStateful) {
175
+ warnings.push({ name: "responses-stateful-unsupported", message: "endpoint 未实证支持 previous_response_id;已发送全量上下文(可 provider.stateful=false 关闭此消息)" })
176
+ }
177
+ chain = null
178
+ } else if (chain && chain.key !== key) {
179
+ chain = null // 跨 turn/压缩/换模型:链失效,全量重建
180
+ } else if (chain && !hostStateful && !opts.forceStateful) {
181
+ chain = null // 非白名单 host 且无显式 forceStateful:不冒险开链
182
+ }
183
+ // 2026-08-31 真机冒烟:百炼/GLM 开链 = 云端留存 7 天——首次知情警告(不刷屏)
184
+ if (wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL) && !provider._responsesStoreWarned) {
185
+ provider._responsesStoreWarned = true
186
+ warnings.push({ name: "responses-store-retention", message: "链生效需要 store:true——对话将在云端留存 7 天(provider.stateful=false 可退出)" })
187
+ }
188
+
189
+ const body = {
190
+ model: provider.model,
191
+ input: items, // 占位:chain 有效时下方替换为增量
192
+ stream: true,
193
+ // 2026-08-31 真机冒烟实锤:百炼/GLM 链要求 R1 store:true(store:false → 链 400
194
+ // Not found);OpenAI 官方 store:false 链仍可用。开链时 = 对话在云端留存 7 天
195
+ // (警告上报);灰名单全量 store:false。
196
+ store: wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL),
197
+ ...(extracted ? { instructions: extracted } : {}),
198
+ ...(toolsFlat ? { tools: toolsFlat } : {}),
199
+ ...(spec.maxOutput || provider.maxTokens ? { max_output_tokens: provider.maxTokens ?? spec.maxOutput } : {}),
200
+ }
201
+ // 内置工具声明追加(2026-08-31 用户拍板):web_search 与本地 function 工具共存
202
+ const builtin = builtinToolsFor(provider.baseURL, provider.builtinTools)
203
+ if (builtin.length) body.tools = [...(toolsFlat ?? []), ...builtin]
204
+ if (provider.temperature != null) {
205
+ let t = provider.temperature
206
+ if (spec.tempRange) {
207
+ t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
208
+ t = Math.round(t * 100) / 100 // 与 core.mjs 同语义(round3 #7)
209
+ }
210
+ body.temperature = t
211
+ }
212
+ if (provider.reasoningEffort) body.reasoning = { effort: provider.reasoningEffort }
213
+ if (opts.toolChoice !== undefined) body.tool_choice = opts.toolChoice
214
+
215
+ // 链模式:turn 内增量 = 上一链轮未发送的 function_call_output(工具结果)。
216
+ // 注意:assistant 的 function_call item 与服务端链输出重复(服务端自动含上轮 output),
217
+ // 增量只发工具结果即可;新 user 消息/压缩/换模型已由 chainKey 挡掉 → 全量。
218
+ const outputs = items.filter((i) => i.type === "function_call_output")
219
+ let previousResponseId = null
220
+ if (chain && chain.id) {
221
+ const newOutputs = outputs.slice(chain.outputSent ?? 0)
222
+ if (newOutputs.length === 0) {
223
+ chain = null // 无新增(重复调用/异常重试):退化为全量,正确性优先
224
+ } else {
225
+ body.input = newOutputs
226
+ previousResponseId = chain.id
227
+ }
228
+ } else {
229
+ body.input = items
230
+ }
231
+
232
+ const newChain = chain
233
+ ? { ...chain, key, outputSent: outputs.length }
234
+ : (wantStateful && (hostStateful || opts.forceStateful) ? { id: null, key, outputSent: outputs.length } : null)
235
+
236
+ return { body, previousResponseId, warnings, newChain }
237
+ }
@@ -11,239 +11,17 @@
11
11
  * 事件流规范:流以 response.completed / response.incomplete / response.failed 结束,
12
12
  * 没有 "data: [DONE]"。
13
13
  */
14
- import { specForModel, isBailianHost } from "../config.mjs"
15
14
  import { effectiveFetchTimeoutMs } from "./core.mjs"
16
15
  import { proxyFetch } from "../proxy.mjs"
17
16
  import { requestWithRetry } from "./retry.mjs"
18
17
  import { rateGate, recordRate, estimateRequestTokens } from "./rate.mjs"
18
+ import { buildBody, normalizeUsage } from "./responses-request.mjs"
19
+ // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):请求构造面出档 `provider/responses-request.mjs`;
20
+ // `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名既有导出经本档转口——消费者 import 面零改。
21
+ export { buildBody, isStoreRequiredHost, builtinToolsFor } from "./responses-request.mjs"
19
22
 
20
23
  // 2026-09-01:FETCH_TIMEOUT_MS 常量退役(绝对墙钟废除)——经 core.mjs effectiveFetchTimeoutMs 共用
21
24
 
22
- /** 白名单:已实证 previous_response_id 的官方端(2026-08-31 真机验证:
23
- * 百炼 store:true 全链路 ✅;GLM(open.bigmodel.cn/api/v1)store:true 全链路 ✅)。 */
24
- function isStatefulHost(baseURL) {
25
- try {
26
- const host = new URL(baseURL).hostname
27
- return /(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
28
- } catch {
29
- return false
30
- }
31
- }
32
-
33
- /** store 必开 host(链保留依赖 store:true——真机:百炼 store:false → 链 400;GLM 同)。
34
- * OpenAI 官方 store:false 链仍可用,不在内。 */
35
- export function isStoreRequiredHost(baseURL) {
36
- try {
37
- const host = new URL(baseURL).hostname
38
- return isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
39
- } catch {
40
- return false
41
- }
42
- }
43
-
44
- /** 灰名单:格式完整但链未证实/不支持——显式全量 + 一次性 warning(不靠服务端报错)。
45
- * 2026-08-31 真机后仅剩 DeepSeek(官方明确 previous_response_id 不支持且参数静默忽略)。 */
46
- function isNonStatefulHost(baseURL) {
47
- try {
48
- const host = new URL(baseURL).hostname
49
- return /(^|\.)deepseek\.com$/.test(host)
50
- } catch {
51
- return false
52
- }
53
- }
54
-
55
- /** 链 key:system 部分 + 最后一条 user 消息(turn 内不变、跨 turn 变、压缩后变)。 */
56
- function chainKey(messages) {
57
- let sig = "s:"
58
- let lastUser = ""
59
- for (const m of messages ?? []) {
60
- if (m.role === "system") sig += (typeof m.content === "string" ? m.content : "") + "\u0001"
61
- else if (m.role === "user") lastUser = typeof m.content === "string" ? m.content : ""
62
- }
63
- return sig + "\u0002u:" + lastUser
64
- }
65
-
66
- /** 内置工具声明(2026-08-31 用户拍板"内置工具还是要用",一期 web_search)。
67
- * 按 host 映射默认集;provider.builtinTools === false 关闭、数组显式覆盖。
68
- * 注意:内置工具由**服务端执行**——绕过我们的工具权限门/审计(产品决策,用户拍板)。 */
69
- export function builtinToolsFor(baseURL, providerBuiltin) {
70
- if (providerBuiltin === false) return []
71
- if (Array.isArray(providerBuiltin)) return providerBuiltin
72
- try {
73
- const host = new URL(baseURL).hostname
74
- if (/(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)deepseek\.com$/.test(host)) {
75
- return [{ type: "web_search" }]
76
- }
77
- } catch { /* fallthrough */ }
78
- return []
79
- }
80
-
81
- /** OpenAI Chat 消息 → Responses input items。system 提升为 instructions(不进 input)。
82
- * 内置工具结果(web_search_call 本地化 tool 消息)→ 原样 web_search_call item 回传。 */
83
- function toItems(messages, { instructions } = {}) {
84
- const items = []
85
- for (const m of messages ?? []) {
86
- if (m.role === "system") continue
87
- if (typeof m.tool_call_id === "string" && m.tool_call_id.startsWith("web_search_call_") && typeof m.content === "string") {
88
- // 内置工具结果本地化消息 → 原样回传(DeepSeek 官方:web_search_call 原样回传即可,
89
- // 服务端自动恢复搜索结果)。id 用 content 里的原始服务端 id(msg_xxx),前缀只是本地锚点。
90
- let query = ""
91
- let srcs = []
92
- let wsId = m.tool_call_id.slice("web_search_call_".length)
93
- try {
94
- const parsed = JSON.parse(m.content)
95
- query = parsed.query ?? ""
96
- srcs = parsed.sources ?? []
97
- if (parsed.id) wsId = parsed.id
98
- } catch { /* content 非 JSON(纯展示)→ query 缺省 */ }
99
- items.push({ type: "web_search_call", id: wsId, status: "completed", action: { query, type: "search", sources: srcs } })
100
- continue
101
- }
102
- if (m.role === "user") {
103
- const content = m.content
104
- if (Array.isArray(content)) {
105
- items.push({
106
- role: "user",
107
- content: content
108
- .map((p) => (p?.type === "image_url"
109
- ? { type: "input_image", image_url: p.image_url?.url }
110
- : p?.type === "text" || typeof p === "string"
111
- ? { type: "input_text", text: typeof p === "string" ? p : p.text }
112
- : null))
113
- .filter(Boolean),
114
- })
115
- } else {
116
- items.push({ role: "user", content: [{ type: "input_text", text: String(content ?? "") }] })
117
- }
118
- } else if (m.role === "assistant") {
119
- const tcList = m.tool_calls ?? []
120
- items.push({ role: "assistant", content: [{ type: "output_text", text: String(m.content ?? "") }] })
121
- for (const tc of tcList) {
122
- items.push({
123
- type: "function_call",
124
- call_id: tc.id ?? "",
125
- name: tc.function?.name ?? "",
126
- arguments: tc.function?.arguments ?? "{}",
127
- })
128
- }
129
- } else if (m.role === "tool" || m.role === "function") {
130
- items.push({
131
- type: "function_call_output",
132
- call_id: m.tool_call_id ?? m.name ?? "",
133
- output: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
134
- })
135
- }
136
- }
137
- return { items, instructions: instructions ?? (messages ?? []).find((m) => m.role === "system")?.content ?? "" }
138
- }
139
-
140
- /** OpenAI 工具 schema → Responses 扁平 tools。 */
141
- function toTools(tools) {
142
- return (tools ?? []).map((t) => ({
143
- type: "function",
144
- name: t.function?.name ?? t.name,
145
- description: t.function?.description ?? t.description,
146
- parameters: t.function?.parameters ?? t.parameters ?? { type: "object", properties: {} },
147
- }))
148
- }
149
-
150
- /** Responses usage → 内部 cache 字段形状。 */
151
- function normalizeUsage(usage) {
152
- if (!usage) return null
153
- return {
154
- prompt_tokens: usage.input_tokens ?? 0,
155
- completion_tokens: usage.output_tokens ?? 0,
156
- total_tokens: usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
157
- prompt_cache_hit_tokens: usage.input_tokens_details?.cached_tokens ?? 0,
158
- prompt_cache_miss_tokens: Math.max(0, (usage.input_tokens ?? 0) - (usage.input_tokens_details?.cached_tokens ?? 0)),
159
- }
160
- }
161
-
162
- /**
163
- * 组装请求体 + 链状态机决策。
164
- * @returns {{ body, previousResponseId, warnings, newChain: {id,key}|null }}
165
- */
166
- export function buildBody(provider, messages, tools, opts = {}) {
167
- const { instructions: extracted, items } = toItems(messages)
168
- const toolsFlat = tools?.length ? toTools(tools) : undefined
169
- const spec = specForModel(provider.model)
170
- const warnings = []
171
- const wantStateful = provider.stateful !== false && opts.stateful !== false
172
- const hostStateful = isStatefulHost(provider.baseURL)
173
- const hostNonStateful = isNonStatefulHost(provider.baseURL)
174
-
175
- let chain = provider._responsesChain ?? null
176
- const key = chainKey(messages)
177
-
178
- if (!wantStateful) {
179
- chain = null // stateful:false 显式覆盖:残留链(同 session 开过)必须作废
180
- } else if (hostNonStateful && !opts.forceStateful) {
181
- // 灰名单:链不支持/未证实(DeepSeek 静默忽略 → 无声丢上下文)——显式全量 + 一次警告
182
- if (wantStateful) {
183
- warnings.push({ name: "responses-stateful-unsupported", message: "endpoint 未实证支持 previous_response_id;已发送全量上下文(可 provider.stateful=false 关闭此消息)" })
184
- }
185
- chain = null
186
- } else if (chain && chain.key !== key) {
187
- chain = null // 跨 turn/压缩/换模型:链失效,全量重建
188
- } else if (chain && !hostStateful && !opts.forceStateful) {
189
- chain = null // 非白名单 host 且无显式 forceStateful:不冒险开链
190
- }
191
- // 2026-08-31 真机冒烟:百炼/GLM 开链 = 云端留存 7 天——首次知情警告(不刷屏)
192
- if (wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL) && !provider._responsesStoreWarned) {
193
- provider._responsesStoreWarned = true
194
- warnings.push({ name: "responses-store-retention", message: "链生效需要 store:true——对话将在云端留存 7 天(provider.stateful=false 可退出)" })
195
- }
196
-
197
- const body = {
198
- model: provider.model,
199
- input: items, // 占位:chain 有效时下方替换为增量
200
- stream: true,
201
- // 2026-08-31 真机冒烟实锤:百炼/GLM 链要求 R1 store:true(store:false → 链 400
202
- // Not found);OpenAI 官方 store:false 链仍可用。开链时 = 对话在云端留存 7 天
203
- // (警告上报);灰名单全量 store:false。
204
- store: wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL),
205
- ...(extracted ? { instructions: extracted } : {}),
206
- ...(toolsFlat ? { tools: toolsFlat } : {}),
207
- ...(spec.maxOutput || provider.maxTokens ? { max_output_tokens: provider.maxTokens ?? spec.maxOutput } : {}),
208
- }
209
- // 内置工具声明追加(2026-08-31 用户拍板):web_search 与本地 function 工具共存
210
- const builtin = builtinToolsFor(provider.baseURL, provider.builtinTools)
211
- if (builtin.length) body.tools = [...(toolsFlat ?? []), ...builtin]
212
- if (provider.temperature != null) {
213
- let t = provider.temperature
214
- if (spec.tempRange) {
215
- t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
216
- t = Math.round(t * 100) / 100 // 与 core.mjs 同语义(round3 #7)
217
- }
218
- body.temperature = t
219
- }
220
- if (provider.reasoningEffort) body.reasoning = { effort: provider.reasoningEffort }
221
- if (opts.toolChoice !== undefined) body.tool_choice = opts.toolChoice
222
-
223
- // 链模式:turn 内增量 = 上一链轮未发送的 function_call_output(工具结果)。
224
- // 注意:assistant 的 function_call item 与服务端链输出重复(服务端自动含上轮 output),
225
- // 增量只发工具结果即可;新 user 消息/压缩/换模型已由 chainKey 挡掉 → 全量。
226
- const outputs = items.filter((i) => i.type === "function_call_output")
227
- let previousResponseId = null
228
- if (chain && chain.id) {
229
- const newOutputs = outputs.slice(chain.outputSent ?? 0)
230
- if (newOutputs.length === 0) {
231
- chain = null // 无新增(重复调用/异常重试):退化为全量,正确性优先
232
- } else {
233
- body.input = newOutputs
234
- previousResponseId = chain.id
235
- }
236
- } else {
237
- body.input = items
238
- }
239
-
240
- const newChain = chain
241
- ? { ...chain, key, outputSent: outputs.length }
242
- : (wantStateful && (hostStateful || opts.forceStateful) ? { id: null, key, outputSent: outputs.length } : null)
243
-
244
- return { body, previousResponseId, warnings, newChain }
245
- }
246
-
247
25
  /** 链失效回退:404/无效 id → 返回 null 表示"应全量重发";其他失败抛错。 */
248
26
  export function isChainInvalidError(status) {
249
27
  return status === 404 || status === 400
@@ -292,7 +70,7 @@ export async function parseStream(response, { onToken, onReasoning, signal }) {
292
70
  onCompleted: seal,
293
71
  onIncomplete: (resp) => {
294
72
  seal(resp)
295
- // 非长度原因(content_filter 等)不能报成 "length"——agent 层 322 行按原因给用户提示
73
+ // 非长度原因(content_filter 等)不能报成 "length"——agent 层按原因给用户提示(`agent/run-stages.mjs:43`)
296
74
  result.finishReason = resp?.incomplete_details?.reason === "content_filter" ? "content_filter" : "length"
297
75
  },
298
76
  onFailed: (resp) => {
@@ -442,7 +220,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
442
220
  } catch (chainErr) {
443
221
  if (!isChainInvalidError(chainErr?.status)) throw chainErr
444
222
  // 链失效(404/400):先清残留链再重建——D6 语义是真·全量重发(body.input=items)。
445
- // 不清链会走 buildBody 217-224 的增量分支:body.input = 裸 function_call_output 且
223
+ // 不清链会走 buildBody 的增量分支(`responses-request.mjs:220-227`):body.input = 裸 function_call_output 且
446
224
  // previousResponseId 未随 fullBody 带走 → 服务端 call_id 无归属 → 二次 400(2026-08-31 评审 #1)
447
225
  provider._responsesChain = null
448
226
  const fresh = buildBody(provider, messages, tools, { toolChoice, stateful, forceStateful: true })