@thincoder/core 0.10.10 → 0.10.12
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 +36 -2
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent/completion.mjs +4 -1
- package/agent/family-tools.mjs +3 -3
- package/agent/helpers.mjs +13 -11
- package/agent/light-round.mjs +1 -4
- package/agent/record-results.mjs +6 -1
- package/agent/setup-reminders.mjs +1 -1
- package/agent/unbooked-pen.mjs +62 -0
- package/agent-tools/advisor.mjs +15 -31
- package/agent-tools/audit-block.mjs +16 -62
- package/agent-tools/batch-gate.mjs +74 -66
- package/agent-tools/batch-lifecycle.mjs +66 -9
- package/agent-tools/batch-paths.mjs +10 -8
- package/agent-tools/context.mjs +1 -1
- package/agent-tools/design-slots.mjs +72 -12
- package/agent-tools/design-token.mjs +27 -4
- package/agent-tools/digest-budget.mjs +2 -0
- package/agent-tools/doc-check.mjs +3 -14
- package/agent-tools/manifest.mjs +4 -5
- package/agent-tools/review-facts.mjs +35 -81
- package/agent-tools/settings.mjs +65 -5
- package/agent-tools/spawn-gates.mjs +68 -31
- package/agent-tools/subagent-async.mjs +6 -3
- package/agent-tools/subagent-spawn.mjs +42 -61
- package/agent-tools/subagent.mjs +15 -15
- package/browser/launch.mjs +14 -3
- package/browser/queue.mjs +31 -3
- package/browser/session.mjs +4 -0
- package/config-defaults.mjs +8 -0
- package/config-presets.mjs +1 -1
- package/config.mjs +60 -11
- package/doc-check/anchors.mjs +10 -9
- package/doc-check/index.mjs +27 -23
- package/doc-check/paragraph.mjs +119 -0
- package/fts-text.mjs +4 -1
- package/ledger-cmd.mjs +56 -41
- package/ledger-db.mjs +65 -58
- package/ledger-tools.mjs +60 -23
- package/ledger.mjs +13 -11
- package/manifest-discovery.mjs +58 -34
- package/manifest.mjs +34 -21
- package/model-context.mjs +98 -0
- package/model-specs.mjs +121 -50
- package/package.json +2 -2
- package/permission.mjs +3 -1
- package/prompts/advisor-design.md +16 -0
- package/prompts/common.md +12 -1
- package/prompts/discipline-engineering.md +42 -12
- package/prompts/discipline-normal.md +8 -7
- package/prompts/persona-coder.md +8 -1
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +26 -4
- package/prompts/persona-engineering.md +37 -38
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/provider/anthropic.mjs +127 -4
- package/provider/core.mjs +16 -6
- package/provider/google.mjs +8 -8
- package/provider/list-models.mjs +37 -5
- package/provider/normalize.mjs +2 -2
- package/provider/responses.mjs +7 -7
- package/provider/sse.mjs +6 -5
- package/proxy-transport.mjs +5 -3
- package/proxy.mjs +2 -1
- package/session-guard.mjs +7 -1
- package/session-lifecycle.mjs +23 -8
- package/session-segments.mjs +1 -0
- package/session-slot-claims.mjs +4 -0
- package/session-slot-write.mjs +27 -1
- package/session-slots-manifest.mjs +25 -4
- package/token-ttl.mjs +78 -4
- package/tool-docs/advisor.md +24 -5
- package/tool-docs/doc_check.md +23 -16
- package/tool-docs/ledger.md +2 -2
- package/tool-docs/subagent.md +41 -16
- package/tool-docs/wait_for.md +15 -6
- package/tools/web-deepseek.mjs +26 -11
- package/doc-check/version.mjs +0 -84
- package/doc-check/width.mjs +0 -87
package/manifest.mjs
CHANGED
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
* - discoverProjects(cwd) → { kind, root, candidates, matched }:**项目梯**(五级 / git 非前提——KD-M1-23);
|
|
22
22
|
* `discoverRepos(cwd)` 同形态 = **仓梯**(`.git` 视图——`git` 工具经它接线,KD-M1-22)。
|
|
23
23
|
* 两梯共一模块私有 walk 内核 `scanChildren`(单源);`candidates` 按名排序、`matched` 记档位。
|
|
24
|
-
* - owningProject(target) →
|
|
24
|
+
* - owningProject(target, boundary) → 归属形单点(沿祖先链取**最近带档目录**;界由调用方定——KD-M1-30)。
|
|
25
25
|
* - projectView(target) → { state, root, path, manifest?, candidates?, errors?, matched }:**按用点解析**
|
|
26
26
|
* 的读侧单点(归属 ∨ 发现兜底 / 五态 / 非抛错 / **零写**——KD-M1-24)。
|
|
27
|
-
* - resolveProjectRoot(cwd)
|
|
27
|
+
* - resolveProjectRoot(cwd):项目根(**归属形**——KD-M1-30)= `projectRootView(cwd).root`(薄委托;取值规则见下行);
|
|
28
28
|
* projectRootView(cwd) → { state: ok|workspace|none, root, candidates }:项目根直读形(判定单点——§2.2)。
|
|
29
29
|
* - resolveEngineeringManifest(cwd, { writer, init }) → 入口决策树(**非抛错**——KD-M1-20):
|
|
30
|
-
* 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1
|
|
30
|
+
* 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——态码五枚
|
|
31
31
|
* `missing` / `invalid` / `no-project` / `workspace` / `init-failed`(KD-M1-28)。名中 `Engineering` = 历史命名(沿用不改名);消费面 = 两端入口钩子 + 翻转面——**非**「模式拥有本模块」。
|
|
32
32
|
* - manifestFilePath(cwd) → 数据档绝对路径(档路径单源 KD-M1-18——读 / 写 / mtime 门控三处同源)。
|
|
33
33
|
* - initManifest(cwd, { writer = 'subagent' } = {}) → 经写门写 DEFAULT_MANIFEST,缺省拒。
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* N3 可迁移:本模块不硬编码任何本仓路径(docRoot 由被开发项目声明;机检参数不在 manifest 面——住引擎内置常量)。
|
|
39
39
|
*/
|
|
40
40
|
import { readFileSync, writeFileSync } from "node:fs"
|
|
41
|
-
import { join, resolve } from "node:path"
|
|
41
|
+
import { join, parse, resolve } from "node:path"
|
|
42
42
|
import { DEFAULT_MANIFEST, isValidDocRootValue, validateManifest, fillDefaults } from "./manifest-schema.mjs"
|
|
43
43
|
import { MANIFEST_REL, discoverProjects, owningProject, resolveProjectRoot } from "./manifest-discovery.mjs"
|
|
44
44
|
// #802(KD-M1-36):未知键读面告警的事件面——诊断日志(fire-and-forget,失败静默降级)。
|
|
@@ -50,10 +50,11 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* **按用点解析**的读侧单点(KD-M1-24 / M1-30——**非抛错 / 零写 / 无缓存**):两段合成——
|
|
53
|
-
* · **归属(第一段——§⑥ 归属形)**:`owningProject(target)` 沿祖先链取最近带档目录
|
|
54
|
-
* (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* · **归属(第一段——§⑥ 归属形)**:`owningProject(target, target 所在盘根)` 沿祖先链取最近带档目录
|
|
54
|
+
* (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。界与 `projectRootView`
|
|
55
|
+
* 同源(目标所在盘根)——同一 target 两面同归属,非会话锚。
|
|
56
|
+
* · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒
|
|
57
|
+
* `self` ⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`workspace` ⇒
|
|
57
58
|
* `workspace`(+ `candidates`);`none` ⇒ `no-project`。
|
|
58
59
|
* 写侧(建档)**不在**此函数(归入口决策树——KD-M1-25 / M1-29)⇒ 注入器 / 只读消费面不会变写点。
|
|
59
60
|
* `matched` 契约:归属段命中 ⇒ `'manifest'`;发现段 ⇒ `discoverProjects.matched` 逐字;
|
|
@@ -65,7 +66,7 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
|
|
|
65
66
|
*/
|
|
66
67
|
export function projectView(target) {
|
|
67
68
|
const anchor = resolve(target ?? ".")
|
|
68
|
-
const owning = owningProject(anchor)
|
|
69
|
+
const owning = owningProject(anchor, parse(anchor).root)
|
|
69
70
|
if (owning) return viewAtRoot(owning, "manifest")
|
|
70
71
|
const d = discoverProjects(anchor)
|
|
71
72
|
if (d.kind === "workspace") return { state: "workspace", root: null, path: null, candidates: d.candidates, matched: d.matched }
|
|
@@ -135,13 +136,13 @@ export function manifestFilePath(cwd) {
|
|
|
135
136
|
/** #802(KD-M1-36)未知键告警去重 memo:档路径 → 上次观测键集(进程内);读到零未知键即清该条。 */
|
|
136
137
|
const unknownKeysSeen = new Map()
|
|
137
138
|
|
|
138
|
-
/**
|
|
139
|
+
/** 未知键诊断(#802 · 2026-10-23 改):**不上界面**——只写 logEvent('manifest:unknown-keys');同档同键集恰一次。 */
|
|
139
140
|
function warnUnknownKeys(file, unknownKeys) {
|
|
140
141
|
if (unknownKeys.length === 0) { unknownKeysSeen.delete(file); return }
|
|
141
142
|
const sig = JSON.stringify([...unknownKeys].sort())
|
|
142
143
|
if (unknownKeysSeen.get(file) === sig) return
|
|
143
144
|
unknownKeysSeen.set(file, sig)
|
|
144
|
-
|
|
145
|
+
// 不上界面(2026-10-23 用户裁定:换别的项目也不能弹,不许骚扰用户);只写诊断日志,`manifest read` 时才出现在结果里
|
|
145
146
|
logEvent("manifest:unknown-keys", { path: file, keys: unknownKeys.join(",") })
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -180,7 +181,7 @@ export function readManifest(cwd) {
|
|
|
180
181
|
|
|
181
182
|
/**
|
|
182
183
|
* 装配钩子入口(模块设计 §2.2)——同 readManifest(cwd)。ok:true 返回补默认值后的 manifest;
|
|
183
|
-
* reason:'missing'
|
|
184
|
+
* reason:'missing' 由调用方走**建档流**(**工程模式会话**口径——梯②⑤ 就地建档,**不拒会话**;
|
|
184
185
|
* KD-M1-29);普通会话 = 装配钩子零 manifest I/O(KD-M1-12)。
|
|
185
186
|
*/
|
|
186
187
|
export function requireManifest(cwd) {
|
|
@@ -205,17 +206,17 @@ export function onManifestInit(fn) {
|
|
|
205
206
|
*
|
|
206
207
|
* 分支(两分支六出口——§2.8 F1 树):
|
|
207
208
|
* 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
|
|
208
|
-
* 缺档 + init →
|
|
209
|
-
* (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥
|
|
209
|
+
* 缺档 + init → 梯②⑤(锚 = 裸仓 / 无项目)⇒ `initManifest(cwd, { writer })`
|
|
210
|
+
* (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥1 项目——1 ∥ 更多同判)⇒
|
|
210
211
|
* `{ ok:false, code:'workspace', message, candidates }`(**不建 / 不猜**)
|
|
211
212
|
* 缺档 + !init → 梯⑤(无项目)⇒ `{ ok:false, code:'no-project' }`(KD-M1-28;`init:false` 面);
|
|
212
|
-
*
|
|
213
|
+
* 梯②(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;工作区 ⇒ 同上
|
|
213
214
|
* 档非法 → { ok:false, code:'invalid', message, errors }
|
|
214
215
|
*
|
|
215
216
|
* `writer` 由调用点**显式**传(生产调用点全传 `'main'`):缺省 `'subagent'` 是写门 fail-closed
|
|
216
|
-
* 缺省(KD-M1-3),误用缺省 ⇒「缺档 +
|
|
217
|
+
* 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
|
|
217
218
|
* AC-20② 语义相反;T38 反证格)。
|
|
218
|
-
*
|
|
219
|
+
* 态码文案分野(KD-M1-28):无项目 ⇒ 可在锚处落地 / 工作区 ⇒ 报明事实 + 点名项目根;文案族 =
|
|
219
220
|
* 「项目不可解析」(稳定锚句 `/项目不可解析/`)——**仅「档非法」格**(§2.8 F2 拒翻面唯一拒格)。
|
|
220
221
|
* 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)由 `projectView` 收为 `invalid`(非抛错
|
|
221
222
|
* 契约——`readManifest` 返回语义零改);本树只承诺上列六出口。
|
|
@@ -236,7 +237,7 @@ export function resolveEngineeringManifest(cwd, { writer = "subagent", init = tr
|
|
|
236
237
|
if (view.state === "workspace") {
|
|
237
238
|
return { ok: false, code: "workspace", candidates: view.candidates, message: workspaceProjectMessage(cwd, view.candidates) }
|
|
238
239
|
}
|
|
239
|
-
//
|
|
240
|
+
// 缺档(梯②⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
|
|
240
241
|
if (!init) return { ok: false, code: view.state === "no-project" ? "no-project" : "missing" }
|
|
241
242
|
try {
|
|
242
243
|
const manifest = initManifest(cwd, { writer })
|
|
@@ -264,16 +265,28 @@ function workspaceProjectMessage(cwd, candidates) {
|
|
|
264
265
|
* 不替被开发项目创建目录(§1.4 边界)——cwd 不存在时写失败自然上抛。
|
|
265
266
|
* @returns {object} 传入的 manifest(校验通过后原样落盘)
|
|
266
267
|
*/
|
|
268
|
+
/** 已退役的顶层键(FR42 八键 → 五键):写入时静默丢弃——它们不是用户拼错,是产品自己退场的键,
|
|
269
|
+
* 留着只会让读面反复报「未知键」骚扰用户(2026-10-23 用户裁定「死键你删掉啊」)。 */
|
|
270
|
+
const RETIRED_KEYS = ["promptsLanding", "checkConfig", "advisor"]
|
|
271
|
+
|
|
272
|
+
/** 返回去掉退役键的新对象(不改入参)。 */
|
|
273
|
+
function stripRetiredKeys(obj) {
|
|
274
|
+
const out = { ...obj }
|
|
275
|
+
for (const k of RETIRED_KEYS) delete out[k]
|
|
276
|
+
return out
|
|
277
|
+
}
|
|
278
|
+
|
|
267
279
|
export function writeManifest(cwd, manifest, { writer = "subagent" } = {}) {
|
|
268
280
|
if (writer !== "main") {
|
|
269
281
|
throw new Error("writeManifest 拒绝:非主 agent 无写权(writer 须为 \"main\",fail-closed 缺省拒)")
|
|
270
282
|
}
|
|
271
|
-
const
|
|
283
|
+
const cleaned = stripRetiredKeys(manifest)
|
|
284
|
+
const validated = validateManifest(cleaned)
|
|
272
285
|
if (!validated.ok) {
|
|
273
286
|
throw new Error(`writeManifest 拒绝:manifest 校验不过——${validated.errors.join(";")}`)
|
|
274
287
|
}
|
|
275
|
-
writeFileSync(manifestFilePath(cwd), JSON.stringify(
|
|
276
|
-
return
|
|
288
|
+
writeFileSync(manifestFilePath(cwd), JSON.stringify(cleaned, null, 2) + "\n")
|
|
289
|
+
return cleaned
|
|
277
290
|
}
|
|
278
291
|
|
|
279
292
|
/**
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-context.mjs — 模型上下文解析的运行时事实面(`PROVIDER.md` §6.15 · 五级链 L1/L3 两层来源)。
|
|
3
|
+
*
|
|
4
|
+
* 组成三件:模块级 `baseURL → (归一 id → 条目表)` 只读缓存 · 键归一(末段 + 小写)· 值域校验
|
|
5
|
+
* (`positiveInt`)。零本地 import(只碰运行时内建)⇒ 零 import 环。
|
|
6
|
+
*
|
|
7
|
+
* - L1 `providers[].modelContext[模型]`(人工声明,**K** 单位)→ `manualContextK`(返 K)
|
|
8
|
+
* - L3 `/models` 自报窗口(**token**)→ `listedContextTokens`(返 token)
|
|
9
|
+
* 单位进函数名:乘点唯一在 `model-specs.mjs`(L1 ∥ L2 ×1024,L3 不乘)——读函数名即知单位。
|
|
10
|
+
*
|
|
11
|
+
* 缓存触发 = 既有列表拉取(写面只在 `provider/list-models.mjs` 成功返回时调 `recordListedContext`):
|
|
12
|
+
* 六个消费面本来就要拉列表,顺带把窗口留在核里,**零新增请求**(D2)。读面是同步纯查表:未命中返
|
|
13
|
+
* `null`、调用侧落下一层,永不抛、永不等待(R28)。
|
|
14
|
+
*
|
|
15
|
+
* 键归一 = 末段 + 小写(规则单源 = `PROVIDER.md` §6.9):`qwen/ZHIPU/GLM-5.3` ⇒ `glm-5.3`。
|
|
16
|
+
* **本面算子 = 末段相等**(不是前缀):用户键 `glm-5.3` 不命中模型 `glm-5.3-flashx`(R29③)。
|
|
17
|
+
* 同侧两条键归一后相撞 = 同一模型的两种写法 ⇒ **命中即取**(对象键序 ∥ 列表序第一条,不排序、
|
|
18
|
+
* 不比较;R29④)——不拒、不告警、不落下一层。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** 值域校验单点(L1 K ∥ L3 token 共用谓词):正整数才收,其余一律 `null`(= 未提供)。
|
|
22
|
+
* 只认 `number`——写面交互输入是字符串,须先 `Number(String(v).trim())` 再判(调用侧责任)。 */
|
|
23
|
+
export function positiveInt(v) {
|
|
24
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0 ? v : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 模型名归一(末段 + 小写——§6.9 三面同一把尺子)。非串归一等价空串(脏载不抛)。 */
|
|
28
|
+
export function normalizeModelKey(name) {
|
|
29
|
+
const m = (typeof name === "string" ? name : "").toLowerCase()
|
|
30
|
+
const slash = m.lastIndexOf("/")
|
|
31
|
+
return slash >= 0 ? m.slice(slash + 1) : m
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeBaseURL(baseURL) {
|
|
35
|
+
return (typeof baseURL === "string" ? baseURL : "").replace(/\/+$/, "")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 告警一次(模块级去重键)——本模块唯一告警出口;去重键决定「一次」的射程。 */
|
|
39
|
+
const warned = new Set()
|
|
40
|
+
export function warnContextOnce(key, message) {
|
|
41
|
+
if (warned.has(key)) return
|
|
42
|
+
warned.add(key)
|
|
43
|
+
console.warn(message)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── L3 缓存:归一 baseURL → (归一 id → 条目表) ───────────────────────────────────
|
|
47
|
+
// 键 = 归一后的 baseURL(列表是端点的事实,不是本地别名的事实——探针目标常缺 name)。
|
|
48
|
+
// 写 = 每次成功拉取**整条替换**(不合并):列表不再报某模型 ⇒ 该模型回到 L4;拉取失败不写、
|
|
49
|
+
// 不清空上次成功值(上次自报比静态表更接近事实)。归一索引在写时建好 ⇒ 读面是一次 `Map` 查表
|
|
50
|
+
// (同键多条目 = 末段相撞,读面取列表序第一条——R29④)。
|
|
51
|
+
const listedByEndpoint = new Map()
|
|
52
|
+
|
|
53
|
+
/** L3 写面(唯一入口):记录一个端点的列表自报窗口表。`entries` = `[id, tokens]` 可迭代
|
|
54
|
+
* (Map ∥ 数组);非法值当场丢弃(= 该模型列表未报)。空表也写入——「这次一条都没报」是事实。 */
|
|
55
|
+
export function recordListedContext(baseURL, entries) {
|
|
56
|
+
const base = normalizeBaseURL(baseURL)
|
|
57
|
+
if (!base) return
|
|
58
|
+
const index = new Map()
|
|
59
|
+
for (const [id, tokens] of entries ?? []) {
|
|
60
|
+
if (typeof id !== "string" || !id) continue
|
|
61
|
+
const value = positiveInt(tokens)
|
|
62
|
+
if (value === null) continue
|
|
63
|
+
const key = normalizeModelKey(id)
|
|
64
|
+
if (!key) continue
|
|
65
|
+
const bucket = index.get(key)
|
|
66
|
+
if (bucket) bucket.push({ id, tokens: value })
|
|
67
|
+
else index.set(key, [{ id, tokens: value }])
|
|
68
|
+
}
|
|
69
|
+
listedByEndpoint.set(base, index)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** L3 读面:列表自报窗口(token,不乘)。未报 ⇒ `null`(落下一层);同端点两 id 末段相撞
|
|
73
|
+
* ⇒ 取列表序第一条即用(同一模型,R29④——不排序、不比较、不告警)。 */
|
|
74
|
+
export function listedContextTokens(provider) {
|
|
75
|
+
const base = normalizeBaseURL(provider?.baseURL)
|
|
76
|
+
const target = normalizeModelKey(provider?.model)
|
|
77
|
+
if (!base || !target) return null
|
|
78
|
+
const bucket = listedByEndpoint.get(base)?.get(target)
|
|
79
|
+
return bucket ? positiveInt(bucket[0].tokens) : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** L1 读面:`providers[].modelContext[模型]` 人工声明(K 单位)。未声明 ∥ 值非法 ⇒ `null`
|
|
83
|
+
* (落下一层);两条键归一后相同 = 同一模型的两种写法 ⇒ 取对象键序第一条即用(R29④——
|
|
84
|
+
* 不排序、不比较、不告警)。值非法在装载期已告警一次(`config.mjs`),此处只判不告(热路径)。 */
|
|
85
|
+
export function manualContextK(provider) {
|
|
86
|
+
const table = provider?.modelContext
|
|
87
|
+
if (!table || typeof table !== "object" || Array.isArray(table)) return null
|
|
88
|
+
const target = normalizeModelKey(provider?.model)
|
|
89
|
+
if (!target) return null
|
|
90
|
+
for (const [key, value] of Object.entries(table)) if (normalizeModelKey(key) === target) return positiveInt(value)
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 测试缝:清空 L3 缓存 + 告警去重(单元档用;生产零调用)。 */
|
|
95
|
+
export function _resetModelContextForTest() {
|
|
96
|
+
listedByEndpoint.clear()
|
|
97
|
+
warned.clear()
|
|
98
|
+
}
|
package/model-specs.mjs
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* config-presets.mjs — the core's single preset face, re-exported by config.mjs.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { listedContextTokens, manualContextK, warnContextOnce } from "./model-context.mjs"
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
|
-
* Known model capability spec table (prefix match, longer first).
|
|
13
|
+
* Known model capability spec table (prefix match within the model's last "/"-segment, longer first).
|
|
12
14
|
* Used for compaction threshold derivation, continuation protocol selection, and capability-aware optimization.
|
|
13
15
|
*
|
|
14
16
|
* context: context window (tokens)
|
|
@@ -42,8 +44,14 @@ const MODEL_SPECS = [
|
|
|
42
44
|
["deepseek-v4-flash-vision-exp", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
|
|
43
45
|
// Kimi series
|
|
44
46
|
["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
+
// kimi-k3.1-pro / kimi-k3.1-pro-highspeed(2026-10-23 发布;官方 quickstart 实读 2026-10-25):
|
|
48
|
+
// context = 1_048_576(官方 1M);maxOutput = 131_072(官方 max_completion_tokens 默认档,上限 1_048_576 不取);
|
|
49
|
+
// **思考始终开启**(官方原文「K3.1 Pro 始终开启思考模式」· FAQ「目前关不了」)⇒ thinkAlwaysOn;推理强度走顶层
|
|
50
|
+
// reasoning_effort low/high/max(默认 max)⇒ thinkApi "effort";多轮 / 工具调用须原样回传完整 assistant message
|
|
51
|
+
// ⇒ reasoningEcho "required";partialMode ✓;视觉 ✓(content 必须是对象数组,图走 base64 ∥ ms://,**不接受公网 URL**)。
|
|
52
|
+
// Highspeed = 同规格、输出约 2~3 倍速(官方「同等智能」)—— 分值分行,靠前缀长度降序先命中。
|
|
53
|
+
["kimi-k3.1-pro", { context: 1_048_576, maxOutput: 131_072, thinking: true, thinkAlwaysOn: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
|
|
54
|
+
["kimi-k3.1-pro-highspeed", { context: 1_048_576, maxOutput: 131_072, thinking: true, thinkAlwaysOn: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
|
|
47
55
|
// Kimi For Coding endpoint uses the short model ID "k3" (same specs as kimi-k3) — IK5VGJ
|
|
48
56
|
["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
|
|
49
57
|
// kimi-for-coding / k3-256k / kimi-for-coding-highspeed(Kimi For Coding 端点三名 · 2026-09-25 批 §14.2 #7-9 · 活测 = §1.4 服务端 200 /models):
|
|
@@ -81,8 +89,7 @@ const MODEL_SPECS = [
|
|
|
81
89
|
["glm-5.3-flashx", { context: 1_000_000, maxOutput: 131_072, thinking: true, thinkAlwaysOn: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
82
90
|
["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
83
91
|
["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
84
|
-
|
|
85
|
-
// glm-4.5-air(智谱轻档 · 2026-09-24 bench 名单批):temperature 0 受理 = **实测**(roster-expand §2.2);其余 = **族沿用 glm-4 行**
|
|
92
|
+
// glm-4.5-air(智谱轻档 · 2026-09-24 bench 名单批):temperature 0 受理 = **实测**(roster-expand §2.2);取值以本行**显式声明**为准
|
|
86
93
|
// (逐名官方口径未取证);枚举 / 视觉面 = **未探**(不声明)。medium 直发实测受理(batch-params-judge §1.5)。
|
|
87
94
|
["glm-4.5-air", { context: 128_000, maxOutput: 32_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
|
|
88
95
|
// GPT series
|
|
@@ -91,11 +98,10 @@ const MODEL_SPECS = [
|
|
|
91
98
|
["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false }],
|
|
92
99
|
["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true }],
|
|
93
100
|
// Qwen series
|
|
94
|
-
// qwen-flash
|
|
101
|
+
// qwen-flash(2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧实测):**尺寸行**——maxOutput 32_768 = **实测**
|
|
95
102
|
// (服务端直报「Range of max_tokens should be [1, 32768]」;131072 / 262144 均 400)。context 未取到 ⇒ 零口径——取兜底值
|
|
96
103
|
// 128_000(与建行前生效值同值,§14.3 兜底语义续效);thinking / 视觉有读数(默认关可开 / 不支持)但不落字段(§14.3 五名口径)。
|
|
97
104
|
["qwen-flash", { context: 128_000, maxOutput: 32_768 }],
|
|
98
|
-
["qwen-vl-max", { context: 128_000, maxOutput: 32_768 }],
|
|
99
105
|
// qwen3.7-max:**实测拒图像**(DashScope 400 "Unexpected item type in content"——真值彩图探针,2026-09-20)
|
|
100
106
|
// ⇒ 无视觉 = **不声明**(T-7 语义单一:multimodal 仅 true / undefined;补探已毕,无需再探)
|
|
101
107
|
["qwen3.7-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
|
|
@@ -137,8 +143,6 @@ const MODEL_SPECS = [
|
|
|
137
143
|
["qwen3.6-flash", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
|
|
138
144
|
// qwen3.6-plus:同形行(共享证据块见 flash 行上)——校验级 maxOutput / 六档枚举、官方口径 context、族沿用机制位。
|
|
139
145
|
["qwen3.6-plus", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
|
|
140
|
-
// qwen3.6-max-preview:同形行——校验级 maxOutput / 六档枚举、官方口径 context、族沿用机制位;视觉通道实测 ✓ 但纯红图答「黑色」(抽样波动,如实记)。
|
|
141
|
-
["qwen3.6-max-preview", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
|
|
142
146
|
// qwen3.6-27b:同形行——校验级 maxOutput / 六档枚举、官方口径 context(未探边)、族沿用机制位。
|
|
143
147
|
["qwen3.6-27b", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
|
|
144
148
|
// qwen3.5-27b(2026-09-24 bench 名单批):context 262_144 = **官方口径(定价页分档佐证——[128K, 256K) 档在售;规格页未逐名核)**;
|
|
@@ -155,8 +159,6 @@ const MODEL_SPECS = [
|
|
|
155
159
|
// always echo); 2026-09-22 re-probe: value / missing field / empty string all 200 — the
|
|
156
160
|
// 2026-09-20 "must be passed back" 400 was NOT reproduced. v2.5 rows aligned 2026-09-22:
|
|
157
161
|
// maxOutput 131_072 = **校验级**.
|
|
158
|
-
["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 131_072, thinking: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
|
|
159
|
-
["mimo-v2.5", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
|
|
160
162
|
// MiMo V2.6 series (2026-09-22 launch — three independent rows, each probed individually).
|
|
161
163
|
// maxOutput 131_072 / tempRange [0, 1.5] = **校验级** (400 "at most 131072 completion tokens" /
|
|
162
164
|
// "temperature must be within [0, 1.5]", per model); thinking = **实测** (bare request carries
|
|
@@ -175,10 +177,9 @@ const MODEL_SPECS = [
|
|
|
175
177
|
// minimax-m3(小写前缀行——与上方 MiniMax-M3 行同值,同族两写法):effort 同按**无生效面**处置
|
|
176
178
|
// (**实测**:effort 被忽略;off 路径 = `thinking:{type:"disabled"}` ✓ rc=0——batch-params-judge §1.2 / KD-32④)。
|
|
177
179
|
["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
|
|
178
|
-
["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, noUsageStream: true }],
|
|
179
180
|
// MiniMax-M2.7(前缀行——覆盖标准档与 -highspeed · 2026-09-24 bench 名单批):thinking = **实测**(裸请求 rc 在场);off 路径 **未找到**
|
|
180
181
|
// (`thinking:{type:"disabled"}` 不生效——如实,不声明 thinkApi);effort 被忽略 ⇒ **无生效面**(枚举不声明;medium 照发仅口径统一);
|
|
181
|
-
// 尺寸 / noUsageStream =
|
|
182
|
+
// 尺寸 / noUsageStream = 本行**显式声明**(逐名官方口径未取证);视觉面 = **未探**(不声明)。
|
|
182
183
|
["MiniMax-M2.7", { context: 256_000, maxOutput: 128_000, thinking: true, noUsageStream: true }],
|
|
183
184
|
// Grok series (xAI — OpenAI-compatible)
|
|
184
185
|
// grok-4.x: 500K context per xAI Grok 4.6 spec (corrected 2026-08; earlier entries said 1M)
|
|
@@ -220,9 +221,6 @@ const MODEL_SPECS = [
|
|
|
220
221
|
// 答 "Unknown"(对照组明说不识图)⇒ 实测无视觉。尺寸位两级逐条标(AC-2):maxOutput 128_000 =
|
|
221
222
|
// **参考实配**(网关对 max_tokens 不硬拒 ⇒ 上限未证);context 256_000 = **网络口径**(他仓/文档 256K,未本渠道实测)。
|
|
222
223
|
["hy3", { context: 256_000, maxOutput: 128_000, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
|
|
223
|
-
// hy3-preview(在场实测:/models + 401 前验活;能力位**未探针**)——**尺寸行**:仅登记尺寸位,取值 =
|
|
224
|
-
// hy3 **同族沿用**(maxOutput 参考实配 / context 网络口径)。D-11(2026-09-25 收窄现口径):能力位**无族据**一律不声明 ⇒ 该名无族据 ⇒ thinking / 枚举 / 视觉全不声明。
|
|
225
|
-
["hy3-preview", { context: 256_000, maxOutput: 128_000 }],
|
|
226
224
|
// hy4-preview(在场实测:/models + 401;能力位**未探针**)——尺寸行:取值 = hy3 族**同族沿用**
|
|
227
225
|
// (maxOutput 参考实配级取值 / context 网络口径;批次档 §1.2 未给该名尺寸行 ⇒ 上报清单 2,行注即标级)。
|
|
228
226
|
["hy4-preview", { context: 256_000, maxOutput: 128_000 }],
|
|
@@ -249,36 +247,32 @@ const MODEL_SPECS = [
|
|
|
249
247
|
]
|
|
250
248
|
const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000 }
|
|
251
249
|
|
|
252
|
-
/**
|
|
250
|
+
/** 查表规则(单源 = PROVIDER.md §6.9 · 用户 2026-10-23 17:34 裁定「匹配 / 拆分以后的最后一段」):
|
|
253
251
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* qwen/ZHIPU/GLM-5.3
|
|
259
|
-
* 行保留为文档锚(发送路径 provider.core isRouter 依赖含 "/" 判定),通用机制已覆盖同类。 */
|
|
252
|
+
* 模型名匹配 = **末段唯一**:按 "/" 拆开只拿最后一段查表(单段名末段 = 自身),段内仍按前缀匹配
|
|
253
|
+
* (`glm-5.3-flashx` 命中 `glm-5.3` 行)。完整名**不再先扫**——旧法把 `glm-5.3/zzz` 误判成
|
|
254
|
+
* `glm-5.3`(1M 上下文 ⇒ 压缩永不触发 ⇒ 溢出);新法取末段 `zzz` ⇒ 未命中 ⇒ DEFAULT_SPEC + 告警。
|
|
255
|
+
* 聚合网关惯例 `vendor/model`(roapi/new-api/one-api/aiproxy)因此天然命中:`zhipu/glm-5.3` /
|
|
256
|
+
* 链式 `qwen/ZHIPU/GLM-5.3` ⇒ `glm-5.3`。末段为空(`foo/`)⇒ 未命中(空串不做匹配)。 */
|
|
260
257
|
const warnedModels = new Set() // warn once per model name — spec lookup runs on every request (hot path)
|
|
261
258
|
// Pre-sorted once at module scope — spec lookup runs on every request (agent, provider core,
|
|
262
259
|
// context, auto-think, TUI rendering); re-sorting per call was wasteful.
|
|
263
260
|
const SORTED_SPECS = [...MODEL_SPECS].sort((a, b) => b[0].length - a[0].length)
|
|
264
261
|
|
|
265
|
-
/** Single table lookup shared by specForModel / specMatch — prefix match (case-insensitive)
|
|
266
|
-
*
|
|
267
|
-
*
|
|
262
|
+
/** Single table lookup shared by specForModel / specMatch — prefix match (case-insensitive) on the
|
|
263
|
+
* model's LAST "/"-segment only (末段唯一 · PROVIDER.md §6.9). The full name is NOT scanned first:
|
|
264
|
+
* `glm-5.3/zzz` takes `zzz` and misses (the old full-name-first scan mis-hit `glm-5.3`).
|
|
265
|
+
* Returns null on a miss (DEFAULT_SPEC is the caller-side fallback and is deliberately NOT
|
|
266
|
+
* returned here — `matched` needs the miss itself).
|
|
268
267
|
* 非串 model(外端脏载 / 手工档)归一等价空串 ⇒ 未命中 ⇒ 调用侧 DEFAULT_SPEC(#638 查表全性:
|
|
269
268
|
* 下游无类型门——`.toLowerCase` 只认串,非串归一后本函数恒 total,同 providerSpec 文档句)。 */
|
|
270
269
|
function lookupSpec(model) {
|
|
271
270
|
const m = (typeof model === "string" ? model : "").toLowerCase()
|
|
272
|
-
for (const [prefix, spec] of SORTED_SPECS) {
|
|
273
|
-
if (m.startsWith(prefix.toLowerCase())) return spec
|
|
274
|
-
}
|
|
275
|
-
// Vendor-namespace strip: take the LAST "/"-separated segment — retry the prefix match on it.
|
|
276
271
|
const slash = m.lastIndexOf("/")
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
272
|
+
const bare = slash >= 0 ? m.slice(slash + 1) : m // 末段唯一;单段名 = 自身
|
|
273
|
+
if (!bare) return null // 末段为空 ⇒ 未命中(空串不做前缀匹配)
|
|
274
|
+
for (const [prefix, spec] of SORTED_SPECS) {
|
|
275
|
+
if (bare.startsWith(prefix.toLowerCase())) return spec
|
|
282
276
|
}
|
|
283
277
|
return null
|
|
284
278
|
}
|
|
@@ -312,27 +306,104 @@ export function specMatch(model) {
|
|
|
312
306
|
return { spec: DEFAULT_SPEC, matched: false }
|
|
313
307
|
}
|
|
314
308
|
|
|
309
|
+
/** Context window 展示串,**收 token**(核单源——三端读面同一格式器出词,R31④):`128K` ∥ `1M`。
|
|
310
|
+
* 本批由 CLI 私有 `fmtContextK` 上提——三端各自拼串会漂。服务链上 L3/L4 值(token)的展示;
|
|
311
|
+
* 人工声明(K)的展示走 `formatDeclaredContextK`,一个换算方向一个入口,不混用。 */
|
|
312
|
+
export function formatContextK(tokens) {
|
|
313
|
+
if (tokens >= 1_000_000) return `${Math.round(tokens / 1_048_576)}M`
|
|
314
|
+
return `${Math.round(tokens / 1024)}K`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 人工声明上下文的展示串,**收 K**(`modelContext` ∥ 旧键 `context` 的存储单位):`128` ⇒ `128K`、
|
|
318
|
+
* `1024` ⇒ `1M`。K→token 的展示换算**只此一处**(内部转 token 再交给 `formatContextK`)——三端读面
|
|
319
|
+
* 把已声明原值直接交进来,不自己换算、不自己拼 K 串(PROVIDER.md §6.15「读面回显」· R31④)。
|
|
320
|
+
* 前提:`k` 是先过 `positiveInt` 的正整数(与读链同一谓词)——非法值本函数不复判。 */
|
|
321
|
+
export function formatDeclaredContextK(k) {
|
|
322
|
+
return formatContextK(k * 1024)
|
|
323
|
+
}
|
|
324
|
+
|
|
315
325
|
/**
|
|
316
|
-
* providerSpec(provider) — spec with
|
|
326
|
+
* providerSpec(provider) — spec with the context resolution chain (PROVIDER.md §6.15, 2026-10-23
|
|
327
|
+
* 五级链). `providerSpecResolution` = the single traversal; `providerSpec` = `resolution.spec`.
|
|
328
|
+
*
|
|
329
|
+
* Chain (first available wins) — 单位差只在这里乘一次,且**解析乘点**全仓唯一(本函数内 L1 ∥ L2 共用;
|
|
330
|
+
* 展示面的另一次在 `formatDeclaredContextK`,不回写解析值):
|
|
331
|
+
* L1 `providers[].modelContext[模型]`(人工声明,K) → ×1024
|
|
332
|
+
* L2 `providers[].context`(旧键,K;仅未被 L1 点名的模型) → ×1024(同一乘点)
|
|
333
|
+
* L3 `/models` 自报(三字段任一,token) → 不乘
|
|
334
|
+
* L4 `MODEL_SPECS` 静态表(token) → 不乘
|
|
335
|
+
* L5 `DEFAULT_SPEC`(128K,token) → 不乘
|
|
336
|
+
*
|
|
337
|
+
* L2 位于 L3 之上:旧键也是人工声明(2026-10-23 17:30 裁定「人工声明 > 列表」),只是粒度不对、
|
|
338
|
+
* 单位事故出过两次;`modelContext` 点名的模型一律走 L1,L2 不参与。L2 实际被用到时告警一次
|
|
339
|
+
* (provider 名去重——装载期告警会在 L1 已完全覆盖时误报)。
|
|
317
340
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* HERE and nowhere else.
|
|
341
|
+
* 量级守卫(R32):L1 ∥ L2 的 K 值换算后 > 静态表 ×4(表无行时 = 4 × 128K)⇒ 照用 + 告警一次,
|
|
342
|
+
* 不截断、不拒写——与 L3 的 4× 守卫对称。守卫住在核里:值可能来自手改 JSON(不经任何界面),
|
|
343
|
+
* 端上守不到;手改 ∥ CLI ∥ VSC ∥ 桌面四条来源在这里同判。
|
|
322
344
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
345
|
+
* 同步纯函数、零 IO:L3 读 `model-context.mjs` 的模块级只读缓存(写面在 listModels 成功拉取时),
|
|
346
|
+
* 未命中即落下一层——providerSpec 在请求热路径上(每请求算预算),async 等于把 IO 塞进请求路径。
|
|
325
347
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
348
|
+
* Returns a COPY ({ ...spec, context }) when L1 / L2 / L3 supplies the window — the shared spec
|
|
349
|
+
* object from SORTED_SPECS ∥ DEFAULT_SPEC must never be mutated, or the override would leak across
|
|
350
|
+
* providers (T-C1). No L1-L3 value ⇒ the shared spec object itself (zero-copy hot path). `maxOutput`
|
|
351
|
+
* 与其余能力位仍取静态表 ∥ DEFAULT_SPEC(需求边界:不改 maxOutput 来源)。
|
|
352
|
+
*
|
|
353
|
+
* `source` 闭集五值(`windowSource` 的唯一来源,不并类):`modelContext` ∥ `provider-override` ∥
|
|
354
|
+
* `list` ∥ `spec` ∥ `default`。`warnUnknownModel` 只在链落到 L4/L5 时触发(L1-L3 供值时 128K 告警为假)。
|
|
330
355
|
*/
|
|
356
|
+
export function providerSpecResolution(provider) {
|
|
357
|
+
const model = provider?.model ?? ""
|
|
358
|
+
const hit = lookupSpec(model)
|
|
359
|
+
const base = hit ?? DEFAULT_SPEC
|
|
360
|
+
// L1 — 人工声明(按模型,K 单位);L2 — 旧键(渠道级,K 单位)。两种「人工声明」粒度在这里合流:
|
|
361
|
+
// **解析面的 ×1024 只出现在合流后的那一次换算上**(L3 起是 token,不乘)——加第二个解析乘点 = 下一次单位事故。
|
|
362
|
+
// (展示面的 K→token 换算另在 `formatDeclaredContextK`,只出词、不回写解析值。)
|
|
363
|
+
const manualK = manualContextK(provider)
|
|
364
|
+
const legacyRaw = Number(provider?.context) // Number() 接受数字字符串("128")——旧键既有口径(code review #1)
|
|
365
|
+
const legacyK = Number.isInteger(legacyRaw) && legacyRaw > 0 ? legacyRaw : null
|
|
366
|
+
const kUnit = manualK ?? legacyK
|
|
367
|
+
if (kUnit !== null) {
|
|
368
|
+
if (manualK === null) {
|
|
369
|
+
warnContextOnce(
|
|
370
|
+
`legacy-context\0${provider?.name ?? ""}`,
|
|
371
|
+
`[config] provider "${provider?.name}" uses the legacy context override (K units) for models not listed in modelContext — migrate to modelContext: { "${model}": ${legacyK} } (K units, same as this key)`,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
// R32 — 人工声明的量级守卫(L1 ∥ L2,与 L3 的守卫对称):换算后 > 静态表 ×4 ⇒ 照用 + 告警一次,
|
|
375
|
+
// **不截断、不拒写**(异常值也可能为真;截断 = 静默改写真事实)。守卫住核——值可能来自手改 JSON
|
|
376
|
+
// (不经任何界面),端上守不到;四条来源(手改 / CLI / VSC / 桌面)在这里同判。去重键 = 渠道 + 模型 + K 值。
|
|
377
|
+
const declaredTokens = kUnit * 1024
|
|
378
|
+
if (declaredTokens > base.context * 4) {
|
|
379
|
+
warnContextOnce(
|
|
380
|
+
`declared-over-4x\0${provider?.name ?? ""}\0${model}\0${kUnit}`,
|
|
381
|
+
`[config] model "${model}" on provider "${provider?.name ?? ""}" declares context ${kUnit}K (= ${declaredTokens} tokens), over 4x the spec value ${base.context} — using the declared value as-is (not truncated)`,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
return { spec: { ...base, context: declaredTokens }, source: manualK !== null ? "modelContext" : "provider-override" }
|
|
385
|
+
}
|
|
386
|
+
// L3 — 列表自报(token,不乘);异常大值照用 + 告警一次(R27——截断 = 静默改写真事实)
|
|
387
|
+
const listedTokens = listedContextTokens(provider)
|
|
388
|
+
if (listedTokens !== null) {
|
|
389
|
+
const staticValue = base.context
|
|
390
|
+
if (listedTokens > staticValue * 4) {
|
|
391
|
+
const endpoint = String(provider?.baseURL ?? "").replace(/\/+$/, "")
|
|
392
|
+
warnContextOnce(
|
|
393
|
+
`listed-over-4x\0${endpoint}\0${model}\0${listedTokens}`,
|
|
394
|
+
`[config] model "${model}" on ${endpoint} reports context ${listedTokens}, over 4x the spec value ${staticValue} — using the reported value as-is (not truncated)`,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
return { spec: { ...base, context: listedTokens }, source: "list" }
|
|
398
|
+
}
|
|
399
|
+
// L4 — 静态表命中;L5 — DEFAULT_SPEC 兜底(未知模型告警只在这里)
|
|
400
|
+
if (hit) return { spec: hit, source: "spec" }
|
|
401
|
+
warnUnknownModel(model)
|
|
402
|
+
return { spec: DEFAULT_SPEC, source: "default" }
|
|
403
|
+
}
|
|
404
|
+
|
|
331
405
|
export function providerSpec(provider) {
|
|
332
|
-
|
|
333
|
-
const k = Number(provider?.context) // Number() 接受数字字符串("128")——两端语义统一(code review #1)
|
|
334
|
-
if (Number.isInteger(k) && k > 0) return { ...spec, context: k * 1024 }
|
|
335
|
-
return spec
|
|
406
|
+
return providerSpecResolution(provider).spec
|
|
336
407
|
}
|
|
337
408
|
|
|
338
409
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thincoder/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.12",
|
|
4
4
|
"description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "node test/run.mjs",
|
|
16
|
-
"prepublishOnly": "
|
|
16
|
+
"prepublishOnly": "node scripts/check-syntax.mjs"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
package/permission.mjs
CHANGED
|
@@ -35,7 +35,9 @@ export function formatPermission(name, args) {
|
|
|
35
35
|
return `${args.path}\n${oldLines}\n ↓\n${newLines}`
|
|
36
36
|
}
|
|
37
37
|
if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
|
|
38
|
-
|
|
38
|
+
// 工程角色无任务书文本参数(任务书 = 批次档 §2):拼装空 ⇒ 回退到 §2 路径句,人审面不空白
|
|
39
|
+
// (AGENT-LOOP-SUBAGENT.md §6.22.8 P11)。
|
|
40
|
+
if (base === "subagent") return cap(composeTaskBookBrief(args, args.role) || (args.batchDoc ? `task book: batch record §2 (${args.batchDoc})` : "") || args.task || "", 500)
|
|
39
41
|
if (base === "memory") {
|
|
40
42
|
// §6 action-routed preview: put shows content, batch delete/clear show the gate args
|
|
41
43
|
const action = String(args.action ?? "")
|
|
@@ -26,6 +26,10 @@ Evaluate the design against these dimensions:
|
|
|
26
26
|
4. **Clarity**: Is the design specific enough to implement? Are the affected files identified?
|
|
27
27
|
5. **Acceptance criteria**: Are they verifiable? Do they cover normal paths, edge cases, and error conditions?
|
|
28
28
|
6. **Scope**: Is the scope appropriate? Are there opportunities to simplify? Is there scope creep?
|
|
29
|
+
6b. **Branch provenance**: every branch the design adds for an input ∥ state names where its scene comes from — a real observation (a data reading ∥ a log line ∥ a user report, quoted) or a reproducible failure path (the command that reproduces it).
|
|
30
|
+
**Neither given** ⇒ the branch guards a scene that has not happened ⇒ **🔴**.
|
|
31
|
+
**Why**: a branch brings four companions of its own — an alert, copy, cross-end consistency and test material; with no scene none of the four can be verified, and nobody later dares delete the branch.
|
|
32
|
+
**A real defence passes**: give the reproduction path and it walks — this kills the branch that cannot name its scene, not defence itself.
|
|
29
33
|
7. **Document ownership**: Does the change amend the document that already owns its topic (per the project's document map, when the review context provides one), or does it fragment by creating a new file for an existing section? Does the wording duplicate or contradict existing documents?
|
|
30
34
|
8. **Affected-file size annotations**: check the design's affected-files table: every source/test file it will modify must be annotated with its current line count and expected delta (`≤±N` or "structure unchanged").
|
|
31
35
|
**Pure `.md` documents are exempt** from that annotation.
|
|
@@ -45,6 +49,18 @@ Evaluate the design against these dimensions:
|
|
|
45
49
|
**The answer takes one of three forms**: `existing: …` · `none found: …` · `existing but unusable: …`.
|
|
46
50
|
**Unused existing piece**: an existing piece left unused with no reason it is unusable ⇒ 🔴.
|
|
47
51
|
**A missing line** stays 🟡.
|
|
52
|
+
12. **Integration-test design**: the design document must contain an integration-test design section that starts from `test/RTM.md` — the RTM: one section per business story, one row per integration case.
|
|
53
|
+
The section answers as a delta against it — which rows this change breaks, which are redundant, which are missing;
|
|
54
|
+
each row names the **interaction between components** each evaluates, the business result each asserts, the project's entry (where a user enters the business — a business-observable point), and the business requirement each traces back to.
|
|
55
|
+
**An integration section that starts from a blank "which scenarios do we need" list, with no RTM read, ⇒ 🔴.**
|
|
56
|
+
A missing row's number is fixed in the RTM before its case title is written — `test('1-1 …')`, the same number verbatim.
|
|
57
|
+
**A design missing that section ⇒ 🔴 (design incomplete)**; a change that alters no business-observable result an existing case asserts and adds no scenario the product really needs states so, with its reason, in that section.
|
|
58
|
+
**Only one reason stands: this change alters no business-observable result an existing case asserts and adds no scenario the product really needs. Anything else — e.g. the 50–100 case window being full — is still 🔴.**
|
|
59
|
+
**A case grows from exactly two entries**: a business scenario the product really needs, or a problem production really produced — crossing components is not an entry.
|
|
60
|
+
**Why**: no other acceptance run evaluates the business result from a user-visible entry, and the batch's unit-test files are deleted at closeout — a design that never names those scenarios leaves that result with no acceptance at all.
|
|
61
|
+
**A design's acceptance naming a full-suite run ⇒ 🔴 (the release gate written as a round cost)**: the criterion is the changed-face unit test file or files (`node --test <file> [<file> ...]`), or — when the change has no runnable test — the check you actually ran and its result;
|
|
62
|
+
the integration half is reported in the closeout line, not run here.
|
|
63
|
+
Why: acceptance is what the implementing round runs — a suite there turns the release gate into a round cost, and a repeat run proves nothing new.
|
|
48
64
|
|
|
49
65
|
## Output Format
|
|
50
66
|
Produce a table with your findings:
|
package/prompts/common.md
CHANGED
|
@@ -7,6 +7,7 @@ Artifacts written to the repository (comments, commit messages, docs) follow the
|
|
|
7
7
|
**Speak plainly**: replies and reports to the user must be **readable and repeatable back**.
|
|
8
8
|
For a subagent, the parent is the user.
|
|
9
9
|
**Internal terms / codenames / jargon used bare leave the reader unable to repeat, execute, or relay them** — follow each with a one-line plain explanation.
|
|
10
|
+
**A concept that already has a name keeps that name** — renaming it leaves the reader holding two names for one thing.
|
|
10
11
|
**Ground complex mechanisms in plain words first**, then give precise details as needed.
|
|
11
12
|
Docs / code / review tables follow their own conventions.
|
|
12
13
|
|
|
@@ -39,10 +40,20 @@ Where the check found nothing relevant, write "checked, nothing relevant" and na
|
|
|
39
40
|
**Bounds**: the instruction-precedence ladder stays as it is — that one governs conflict, this one governs the look before acting; the check is narrow — whether the how of this work is already written, not a long survey before every move.
|
|
40
41
|
|
|
41
42
|
## Who you are
|
|
42
|
-
Programming is collaborative labor between you and the human.
|
|
43
43
|
The human decides direction and makes the final call. You own the code — the entire project is your code.
|
|
44
44
|
What you confirm is your contract.
|
|
45
45
|
|
|
46
|
+
**The base every role works from**: where a written rule is silent, these decide; where it speaks, these are its reason.
|
|
47
|
+
- **Programming is collaborative labor**: the human expresses intent and decides; you execute. The code is a joint work, not a request pushed down a pipe.
|
|
48
|
+
- **Say more, not less**: an uncertain report is cheap and a wrong decision is dear; when you cannot tell whether it matters, say it and let the human skip it. Silence and a wrong report are the same dishonesty.
|
|
49
|
+
- **The design doc is the spec, not reference material**: it says what should be; code says what is. When they conflict the doc wins — point the gap out, never overwrite its intent with your own habit.
|
|
50
|
+
- **Official sources are authority; guessing is waste**: an unclear API, protocol or framework behavior gets looked up, not inferred from context or a partial example. A guess is right once and misleads every layer under it.
|
|
51
|
+
- **A finished job comes with its account**: what changed, why, what the risk is, what was not done. The human sees only what you say, so a report that does not match the delivery leaves every decision resting on false information.
|
|
52
|
+
- **Delivery transparency**: state item by item what was fully covered, what was simplified and how, what was left out and why. A gap is not the error — not saying it is.
|
|
53
|
+
- **No step is cut for speed**: you are faster than the human already, so speed was never the bottleneck; what breaks work is a decision made in a hurry. No step cut for time, no check skipped on "probably fine".
|
|
54
|
+
- **A related problem is not extra work**: change a signature and you change the call sites; a bug you can see next door gets fixed and reported. That is finishing the job, not over-delivering.
|
|
55
|
+
- **Do not decide for the human, but do not under-inform them**: a scope too wide, an architecture call, a possible clash with intent — lay the options out and let them choose; you prepare the decision, they make it.
|
|
56
|
+
|
|
46
57
|
## When choices conflict
|
|
47
58
|
- Correctness first. Rushing gets paid back in rework, so speed is not the bottleneck.
|
|
48
59
|
- Debatable choices → lay out options. Better approach → recommend with specifics.
|