@thincoder/core 0.10.12 → 0.10.13
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 +13 -0
- package/agent/run-stages.mjs +33 -29
- package/agent/setup.mjs +3 -0
- package/agent/turn-loop.mjs +3 -0
- package/agent-tools/batch-lifecycle.mjs +16 -3
- package/agent-tools/batch-skeleton.mjs +9 -1
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent.mjs +1 -0
- package/client-identity.mjs +54 -0
- package/config.mjs +19 -13
- package/context.mjs +5 -5
- package/doc-check/index.mjs +28 -15
- package/doc-check/paragraph.mjs +6 -3
- package/embedding.mjs +2 -0
- package/generate-title.mjs +3 -2
- package/ledger-close-proof.mjs +80 -0
- package/ledger-cmd.mjs +32 -23
- package/ledger-tools.mjs +4 -4
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/prompts/persona-engineering.md +2 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +19 -3
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +10 -4
- package/provider/list-models.mjs +4 -3
- package/provider/responses.mjs +17 -7
- package/session-lifecycle.mjs +3 -0
- package/token-window.mjs +23 -2
package/provider/google.mjs
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { proxyFetch } from "../proxy.mjs"
|
|
8
|
+
import { withUserAgent } from "../client-identity.mjs"
|
|
8
9
|
import { requestWithRetry } from "./retry.mjs"
|
|
9
10
|
import { effectiveFetchTimeoutMs, effectiveBodyIdleMs } from "./core.mjs"
|
|
10
11
|
import { abortError, timeoutError } from "../abort-provenance.mjs"
|
|
11
12
|
import { terminateBody } from "../stream-destroy.mjs"
|
|
12
13
|
import { requestBudget } from "../config.mjs"
|
|
13
14
|
import { DEFAULT_BODY_IDLE_MS } from "../config-defaults.mjs"
|
|
15
|
+
import { assertPromptFits } from "./errors.mjs"
|
|
14
16
|
|
|
15
17
|
/** OpenAI 语义 tool_choice → Gemini FunctionCallingConfig(2026-08-31 能力层)。 */
|
|
16
18
|
function mapFunctionCallingConfig(choice) {
|
|
@@ -77,7 +79,7 @@ export function convertMessages(messages) {
|
|
|
77
79
|
|
|
78
80
|
/** Build and send a Gemini chat request. Returns the same shape as core.mjs chat.
|
|
79
81
|
* 2026-08-31 会诊 #6:接入 rateGate/recordRate(原实现完全绕过 TPM/RPM 闸门)。 */
|
|
80
|
-
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice }) {
|
|
82
|
+
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, logCtx }) {
|
|
81
83
|
const systemMessages = messages.filter((m) => m.role === "system")
|
|
82
84
|
const contents = convertMessages(messages)
|
|
83
85
|
|
|
@@ -118,7 +120,10 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
118
120
|
|
|
119
121
|
// 会诊 #6:TPM/RPM 闸门 + 记账
|
|
120
122
|
const { rateGate, recordRate, estimateRequestTokens } = await import("./rate.mjs")
|
|
121
|
-
|
|
123
|
+
// §6.17 发送面净判:估准面补 `tools`(原只投影 messages);body 用 `maxOutputTokens`、无 `max_tokens` 键 ⇒ 减项恒 0
|
|
124
|
+
const estimated = estimateRequestTokens({ messages, tools })
|
|
125
|
+
const estPrompt = estimated - (body.max_tokens ?? 0)
|
|
126
|
+
assertPromptFits(provider, estPrompt, logCtx)
|
|
122
127
|
await rateGate(provider, estimated, onWait, signal)
|
|
123
128
|
|
|
124
129
|
// 2026-08-31:5xx/网络与 OpenAI 格式统一退避重试链(原完全无重试——Gemini 高峰
|
|
@@ -126,7 +131,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
126
131
|
const response = await requestWithRetry(
|
|
127
132
|
() => proxyFetch(url, {
|
|
128
133
|
method: "POST",
|
|
129
|
-
headers: { ...(provider.headers
|
|
134
|
+
headers: { ...withUserAgent(provider.headers), "Content-Type": "application/json" }, // 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17);内置头在后故同名胜出
|
|
130
135
|
body: JSON.stringify(body),
|
|
131
136
|
// 2026-09-01:同 core.mjs——绝对墙钟废除;响应头阶段 fetchTimeoutMs(600s 默认),body 阶段读侧 idle 管
|
|
132
137
|
signal,
|
|
@@ -145,6 +150,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
145
150
|
content: result.content,
|
|
146
151
|
reasoning: result.reasoning,
|
|
147
152
|
finishReason: result.finishReason ?? null, // §6.31.7 F-DA5(原三 transport 恒 null——字段值收正)
|
|
153
|
+
estimated: estPrompt, // §6.17 自校准采样(den 与净判同口径)
|
|
148
154
|
usage: {
|
|
149
155
|
prompt_tokens: usage.prompt_tokens ?? 0,
|
|
150
156
|
completion_tokens: usage.completion_tokens ?? 0,
|
|
@@ -154,7 +160,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, toolCalls: result.toolCalls }
|
|
163
|
+
return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, estimated: estPrompt, toolCalls: result.toolCalls }
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/** Gemini `finishReason` ⇒ 核统一 `finishReason`(§6.31.7 F-DA5——归一只喂既有分类;
|
package/provider/list-models.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 解析保持防御性(字段缺失即跳过该项)。候选不过滤非对话模型(embedding 等——§16.6 #14)。
|
|
15
15
|
*/
|
|
16
16
|
import { proxyFetch } from "../proxy.mjs"
|
|
17
|
+
import { withUserAgent } from "../client-identity.mjs"
|
|
17
18
|
import { resolveProviderSecrets } from "../env-ref.mjs"
|
|
18
19
|
import { positiveInt, recordListedContext } from "../model-context.mjs"
|
|
19
20
|
|
|
@@ -74,7 +75,7 @@ function listedWindowOf(row) {
|
|
|
74
75
|
|
|
75
76
|
async function listOpenai(provider, signal) {
|
|
76
77
|
const url = `${provider.baseURL}/models`
|
|
77
|
-
const data = await fetchJson(provider, url, { ...(provider.headers
|
|
78
|
+
const data = await fetchJson(provider, url, { ...withUserAgent(provider.headers), Authorization: `Bearer ${provider.apiKey ?? ""}` }, signal)
|
|
78
79
|
// L3 写面的收集段(PROVIDER.md §6.15):解析 `data[]` 时顺带收 `id → 窗口`——三字段按序取
|
|
79
80
|
// 第一个**合法**值(R25 ∥ R26),非法相当于该字段缺席;三字段都不可得 = 该模型列表未报(不进表)。
|
|
80
81
|
const ids = []
|
|
@@ -92,7 +93,7 @@ async function listOpenai(provider, signal) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
async function listAnthropic(provider, signal) {
|
|
95
|
-
const headers = { ...(provider.headers
|
|
96
|
+
const headers = { ...withUserAgent(provider.headers), "x-api-key": provider.apiKey ?? "", "anthropic-version": ANTHROPIC_VERSION }
|
|
96
97
|
const base = `${provider.baseURL}/models?limit=1000`
|
|
97
98
|
const out = []
|
|
98
99
|
let afterId = null
|
|
@@ -108,7 +109,7 @@ async function listAnthropic(provider, signal) {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
async function listGoogle(provider, signal) {
|
|
111
|
-
const headers = { ...(provider.headers
|
|
112
|
+
const headers = { ...withUserAgent(provider.headers) }
|
|
112
113
|
const base = `${provider.baseURL}/models?key=${encodeURIComponent(provider.apiKey ?? "")}&pageSize=1000`
|
|
113
114
|
const out = []
|
|
114
115
|
let pageToken = null
|
package/provider/responses.mjs
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { effectiveFetchTimeoutMs, effectiveBodyIdleMs } from "./core.mjs"
|
|
15
15
|
import { proxyFetch } from "../proxy.mjs"
|
|
16
|
+
import { withUserAgent } from "../client-identity.mjs"
|
|
16
17
|
import { requestWithRetry } from "./retry.mjs"
|
|
17
18
|
import { rateGate, recordRate, estimateRequestTokens } from "./rate.mjs"
|
|
18
19
|
import { buildBody, normalizeUsage } from "./responses-request.mjs"
|
|
19
|
-
import { classifyResponsesErrorCode } from "./errors.mjs"
|
|
20
|
+
import { classifyResponsesErrorCode, assertPromptFits } from "./errors.mjs"
|
|
20
21
|
// 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):请求构造面出档 `provider/responses-request.mjs`;
|
|
21
22
|
// `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名既有导出经本档转口——消费者 import 面零改。
|
|
22
23
|
export { buildBody, isStoreRequiredHost, builtinToolsFor } from "./responses-request.mjs"
|
|
@@ -223,15 +224,18 @@ function handleEvent(ev, h) {
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
/** 主入口:请求 + 链状态推进(与 core.mjs 的 chat 同形返回)。 */
|
|
226
|
-
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, stateful }) {
|
|
227
|
+
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, stateful, logCtx }) {
|
|
227
228
|
const { body: reqBody, previousResponseId, warnings, newChain } = buildBody(provider, messages, tools, { toolChoice, stateful })
|
|
228
229
|
const body = { ...reqBody, ...(previousResponseId ? { previous_response_id: previousResponseId } : {}) }
|
|
229
230
|
|
|
230
231
|
// rateGate/recordRate 对齐 core(responses body 无 messages 键——按本地全量 messages 估算)
|
|
231
|
-
|
|
232
|
+
// §6.17 发送面净判:估准面补 `tools`(原只投影 messages);body 用 `max_output_tokens`、无 `max_tokens` 键 ⇒ 减项恒 0
|
|
233
|
+
const estimated = estimateRequestTokens({ messages, tools })
|
|
234
|
+
const estPrompt = estimated - (body.max_tokens ?? 0)
|
|
235
|
+
assertPromptFits(provider, estPrompt, logCtx)
|
|
232
236
|
await rateGate(provider, estimated, onWait, signal)
|
|
233
|
-
//
|
|
234
|
-
const headers = { ...(provider.headers
|
|
237
|
+
// 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17):定制头在前、内置头在后——同名内置头胜出(三处 fetch 共用)
|
|
238
|
+
const headers = { ...withUserAgent(provider.headers), "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` }
|
|
235
239
|
|
|
236
240
|
// round2 复验 #1(2026-08-31):retry 层对 4xx 非可重试是 throw 而非返回——
|
|
237
241
|
// requestWithRetry 从不返回 400/404 响应 → 下方 isChainInvalidError 分支原来不可达(D6 死代码)。
|
|
@@ -287,11 +291,17 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
287
291
|
{ signal, onWait, buildMessage: (status, text) => `Responses API error ${status}: ${text}` },
|
|
288
292
|
)
|
|
289
293
|
}
|
|
290
|
-
return finish(provider, response, {
|
|
294
|
+
return finish(provider, response, {
|
|
295
|
+
onToken, onReasoning, signal, newChain, warnings, estimated,
|
|
296
|
+
// §6.17 自校准口径:链式(`previous_response_id`)请求的 body 只带增量(`responses-request.mjs`
|
|
297
|
+
// `body.input = newOutputs`),而分母是本地全量估算 ⇒ 两者口径不同,本轮不采样。
|
|
298
|
+
estPrompt: previousResponseId ? undefined : estPrompt,
|
|
299
|
+
})
|
|
291
300
|
}
|
|
292
301
|
|
|
293
|
-
async function finish(provider, response, { onToken, onReasoning, signal, newChain, warnings, estimated }) {
|
|
302
|
+
async function finish(provider, response, { onToken, onReasoning, signal, newChain, warnings, estimated, estPrompt }) {
|
|
294
303
|
const result = await parseStream(response, { onToken, onReasoning, signal })
|
|
304
|
+
result.estimated = estPrompt // §6.17 自校准采样(den 与净判同口径)
|
|
295
305
|
recordRate(provider, estimated, result.usage)
|
|
296
306
|
// 链状态推进:completed 事件里 response.id 供同一 turn 后续增量;
|
|
297
307
|
// 截断/失败/无 id(部分端点不回传)→ 链作废(后续全量,正确性优先)。
|
package/session-lifecycle.mjs
CHANGED
|
@@ -322,6 +322,9 @@ export function resetSessionState(agent) {
|
|
|
322
322
|
agent._runStartHistoryLen = 0
|
|
323
323
|
agent._lastPromptTokens = null
|
|
324
324
|
agent._usageAtLen = null
|
|
325
|
+
// §6.17 估算自校准:新会话无样本(比 = 1)——跨会话不继承旧模型的估算偏差
|
|
326
|
+
agent._tok = null
|
|
327
|
+
agent._tokRatio = 1
|
|
325
328
|
agent.planMode = false
|
|
326
329
|
agent.goal = null
|
|
327
330
|
agent._pendingReminders = []
|
package/token-window.mjs
CHANGED
|
@@ -138,11 +138,13 @@ export function contextUsage(agent, extras = {}) {
|
|
|
138
138
|
const overhead = system + tools
|
|
139
139
|
const historyTokens = estimateTokens(history)
|
|
140
140
|
const measured = agent?._lastPromptTokens != null
|
|
141
|
+
// §6.3 / §6.17 估算自校准:估算值乘 `_tokRatio`;`_lastPromptTokens` 是服务端真值,不乘。
|
|
142
|
+
const ratio = agent?._tokRatio ?? 1
|
|
141
143
|
return {
|
|
142
144
|
basis: measured ? "measured" : "estimated",
|
|
143
145
|
total: measured
|
|
144
|
-
? agent._lastPromptTokens + estimateTokens(history.slice(agent._usageAtLen ?? history.length))
|
|
145
|
-
: historyTokens + overhead,
|
|
146
|
+
? agent._lastPromptTokens + Math.round(estimateTokens(history.slice(agent._usageAtLen ?? history.length)) * ratio)
|
|
147
|
+
: Math.round((historyTokens + overhead) * ratio),
|
|
146
148
|
history: historyTokens,
|
|
147
149
|
system,
|
|
148
150
|
tools,
|
|
@@ -153,6 +155,25 @@ export function contextUsage(agent, extras = {}) {
|
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
/**
|
|
159
|
+
* 估算自校准采样(§6.17「估算自校准」)——池化比:`num += usage.prompt_tokens`、`den += estPrompt`
|
|
160
|
+
* (分子分母同取输入侧口径;`estPrompt` 由发送面随 result 带回,与净判同一个值)。
|
|
161
|
+
* `key = format:model` 变 ⇒ 归零重采(切模型自清,不接四个刷新点);`_tok` / `_tokRatio` 只由本函数写,
|
|
162
|
+
* `/new` 与 run 装配复位。无样本 ⇒ 比 = 1(估算维持原口径)。
|
|
163
|
+
* 作用面 = `contextUsage` 两支承(§6.3);实测基线 `_lastPromptTokens` 不乘。
|
|
164
|
+
*/
|
|
165
|
+
export function recordTokenSample(agent, provider, estPrompt, usage) {
|
|
166
|
+
const prompt = Number(usage?.prompt_tokens)
|
|
167
|
+
if (!agent || !(prompt > 0) || !(estPrompt > 0)) return
|
|
168
|
+
const key = `${provider?.format ?? "openai"}:${provider?.model ?? ""}`
|
|
169
|
+
if (agent._tok?.key !== key) agent._tok = { key, num: 0, den: 0 }
|
|
170
|
+
agent._tok.num += prompt
|
|
171
|
+
agent._tok.den += estPrompt
|
|
172
|
+
// 下界 = 1:已知偏差方向是估算偏低;比 < 1 会把全部估算支缩小、重开「该压没压」原始故障,
|
|
173
|
+
// 保守方向不取(仅一侧校准,零新魔数)。
|
|
174
|
+
agent._tokRatio = Math.max(1, agent._tok.num / agent._tok.den)
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
/**
|
|
157
178
|
* 状态行百分比(§6.16.4「与状态行 ctx% 的单源关系」)——与 CLI 状态行(`render-frame.mjs` 式)与 VSC `ctxPercentForHistory` **同式**;
|
|
158
179
|
* stats 报出的就是用户看到的那个百分比(不新增第三口径);空历史 ⇒ 0(状态行的 `ctxPct > 0` 显示门在端侧)。
|