@thincoder/core 0.9.5 → 0.10.2
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.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -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
|
+
}
|
package/provider/responses.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 })
|