@sema-agent/client-core 0.63.2 → 0.64.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +4 -2
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +10 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +15 -1
- package/dist/autoModeUnavailable.d.ts +47 -0
- package/dist/autoModeUnavailable.js +63 -0
- package/dist/classifierStatus.d.ts +101 -0
- package/dist/classifierStatus.js +181 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -0
- package/dist/modelCapabilityProbe.d.ts +248 -0
- package/dist/modelCapabilityProbe.js +291 -0
- package/docs/INTEGRATION-CLIENTS.md +317 -10
- package/package.json +2 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/classifierStatus.ts — auto 分类器的**状态读器**与唯一措辞铸点(0.64.0 件②;
|
|
3
|
+
* core 7.10.0 `AutoModeBreakerTrip` / `WiringManifest.autoMode.breaker`)。
|
|
4
|
+
*
|
|
5
|
+
* -- 它答的是哪一问(与卡面那一问**不是同一问**)----------------------------------------------
|
|
6
|
+
* `autoModeUnavailable.ts` 答的是**卡面**那一问:「这一刻**为什么在问我**」——一次性的、就这只 ask。
|
|
7
|
+
* 本模块答的是**状态面**那一问:「这个**会话**上,auto 分类器现在是什么状态」——诊断行、模型设置页、
|
|
8
|
+
* 权限卡的状态栏读的是它。两问的下一步不同,所以句子也不同(门里有一条反向钉守着两张面的句子零重合)。
|
|
9
|
+
*
|
|
10
|
+
* -- 两处窄读,一只读器 ----------------------------------------------------------------------
|
|
11
|
+
* · **会话轴**:`wiring_manifest.autoMode.breaker`(core `AutoModeBreakerTrip`;server ≥7.69.0 才投)
|
|
12
|
+
* —— 「这个会话的闩**已经**合上了,是什么时候、因为什么、连着失败了几次」。
|
|
13
|
+
* · **本轮轴**:一只 ask 上的 `classifierUnavailable.cause` —— 复用 0.63.0 已有的
|
|
14
|
+
* {@link classifierUnavailableOf},**不重铸**。
|
|
15
|
+
* `classifierBreakerOf` 是会话轴那一处的**唯一**窄读:`wiring_manifest` 的投影臂与本模块的
|
|
16
|
+
* 状态读器共用它。各写一份就是两份台账各漂各的(本包一贯要根治的形)。
|
|
17
|
+
* 🔴 **它的物理位置在 `autoModeUnavailable.ts` 而不是这里**,理由是**门**:`wiring_manifest` 的
|
|
18
|
+
* 投影臂在**内核闭包**里,而那道闭包的棘轮判据逐字是「新进来的这一件自己有没有 import ——
|
|
19
|
+
* 有,就说明它不是叶子,那种 +1 正是本棘轮要拦的东西」。本模块有值级 import(它复用本轮轴的
|
|
20
|
+
* 读器,刻意不另铸第二只),所以**本模块自己不该进内核**;而那只窄读是零 import 的纯判据,
|
|
21
|
+
* 放进已经在闭包里的那张**熔断轴表**的同一个文件里,内核只多一件叶子。归属上也更顺:
|
|
22
|
+
* `AUTO_MODE_BREAKER_CAUSES` 本来就在那里,窄读读的正是那条轴。
|
|
23
|
+
*
|
|
24
|
+
* -- 🔴 「有熔断记录」与 `armed`/`reason` 之间**没有**互证关系,别去补一条 ---------------------
|
|
25
|
+
* 直觉会想写「有 breaker ⇒ reason 应当是 `latch_open`」。那条互证是**错的**,而且会把真读数整段判没:
|
|
26
|
+
* core 的 arm 词表头注逐字写着 —— 闩合上之后,**被闩住的那条腿不再把 auto 意图传给后续腿**,于是
|
|
27
|
+
* 后续腿的 arm 判读走的是 `no_intent`;`latch_open` 那个词留在词表里只是为了让「中途重读」的消费者
|
|
28
|
+
* 在闭集上完备,而**今天没有任何东西铸它**。所以一份 `{armed:false, reason:"no_intent", breaker:{…}}`
|
|
29
|
+
* 是**最常见**的合法形,不是矛盾。
|
|
30
|
+
*
|
|
31
|
+
* -- 🔴 优先序承重:本轮事实 > 这条腿的 `armed` > 历史熔断记录 -------------------------------
|
|
32
|
+
* 判据锚在**真正决定「分类器现在跑不跑」的量**上,而那个量是**这条腿的 `armed`**,不是账本里有没有
|
|
33
|
+
* 一条旧记录:
|
|
34
|
+
* · **本轮事实最先**(`classifierUnavailable`)—— 它说的是眼前这一只 ask,最具体也最新;
|
|
35
|
+
* · **其次是 `armed`** —— core 顶注逐字:「A decider is minted per run (its latch is a RUN fact),
|
|
36
|
+
* so a trip names one leg; the ledger below carries the most recent one forward per session」。
|
|
37
|
+
* ⇒ 同一会话的**后一条腿**完全可以重新武装(新 decider 的闩由构造关着),而账本仍把那次旧 trip
|
|
38
|
+
* 带着。`armed === true` 时判 `breaker_open`,就是把一个**正在放行**的分类器显示成已经关掉了
|
|
39
|
+
* (异源对抗复审 r2 抓出的真病);
|
|
40
|
+
* · **最后才是历史记录** —— 没武装、而账本里有一条 trip,那条 trip 就是「为什么没武装」的最好解释。
|
|
41
|
+
* 🔴 **历史记录不因为被压下去就丢掉**:任何一态上 {@link ClassifierStatusView.breaker} 都照带,
|
|
42
|
+
* 端可以在「在跑」那一行后面补一句「这个会话上曾经熔断过」——那是**两条不同的下一步**,所以
|
|
43
|
+
* 措辞铸点为它单出一句。
|
|
44
|
+
*
|
|
45
|
+
* -- 🔴 时刻一律 UTC ISO,不猜用户时区 --------------------------------------------------------
|
|
46
|
+
* 时区是宿主的事(它知道用户的 locale,本包不知道)。一个猜错的本地时刻比一个明确的 UTC 时刻更难
|
|
47
|
+
* 排查 —— 运维拿着它去对引擎日志,而引擎日志也是 UTC。
|
|
48
|
+
* 🔴 `openedAtMs` 是 **wire 来的数**:一个 `1e20` 会让 `toISOString()` 当场抛 `RangeError`,那是
|
|
49
|
+
* **整屏崩**不是一行渲不出([render-path-must-not-throw])。窄读因此按 Date 的真实值域收。
|
|
50
|
+
*
|
|
51
|
+
* -- 退役条款 ---------------------------------------------------------------------------------
|
|
52
|
+
* 上游若改采 CC 形而把 `breaker` 这一键退役(它今天是 sema 在 CC 之外自己加的一格),本模块**零改**:
|
|
53
|
+
* {@link classifierBreakerOf} 的**缺席臂**当天就是正解 —— 读不到就是没有,状态面自然回落到本轮轴
|
|
54
|
+
* 与 `available`。这条设计是刻意的:退役一个 additive 键不该逼三端各改一次。
|
|
55
|
+
*/
|
|
56
|
+
import { classifierBreakerOf, classifierUnavailableOf } from './autoModeUnavailable.js';
|
|
57
|
+
/**
|
|
58
|
+
* 分类器的三态。**刻意只有三个** —— 「没武装」不是分类器的健康状态,那一问由
|
|
59
|
+
* `wiring_manifest.autoMode.reason` 那一格自己回答(六词闭集),在这里再答一遍会长出第二份台账。
|
|
60
|
+
|
|
61
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]`(同
|
|
62
|
+
* `AUTO_MODE_UNAVAILABLE_CAUSES` 的理由)。
|
|
63
|
+
*/
|
|
64
|
+
export const CLASSIFIER_STATUS_STATES = Object.freeze([
|
|
65
|
+
'available',
|
|
66
|
+
'breaker_open',
|
|
67
|
+
'unavailable_this_round',
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* 「这个会话上,auto 分类器现在是什么状态」——三态,或 `undefined`(**说不出来**)。
|
|
71
|
+
*
|
|
72
|
+
* @param autoMode `wiring_manifest` 的 `autoMode` 段(投影后的或原始的都吃;本函数自己窄读)
|
|
73
|
+
* @param ask 可选:**本轮**那只 ask(或 durable park 行的 `tool_approval` 载荷)
|
|
74
|
+
*
|
|
75
|
+
* 优先序(承重,理由见模块顶注):**本轮事实 > 这条腿的 `armed` > 历史熔断记录**——先看 `ask` 上的本轮
|
|
76
|
+
* 不可用事实,再看这条腿武没武装(武装了 = 分类器真的在跑,压过账本里的历史记录),最后才拿历史
|
|
77
|
+
* 熔断记录说话。`ask` 带本轮事实时,`autoMode` 段缺席也照样答 `unavailable_this_round`。
|
|
78
|
+
*
|
|
79
|
+
* 🔴 **`undefined` 是一个诚实的答案,不是一个坏路径**:
|
|
80
|
+
* · `autoMode` 段缺席(老 mint / 外部 derive)⇒ 这一端**没有**分类器的健康读数;
|
|
81
|
+
* · 段在、但没武装,而且既没有本轮失败事实、账本里也没有一条 trip ⇒ 分类器**压根没参与**这条腿,
|
|
82
|
+
* 它的健康无从谈起。
|
|
83
|
+
* 两种情形都**绝不**折成 `available`(那是把「没报」渲成「一切正常」)。
|
|
84
|
+
* ⚠️ 「没武装」本身仍是一条要渲的事实 —— 但它的出处是 `autoMode.reason`,不是本读器。
|
|
85
|
+
*/
|
|
86
|
+
export function classifierStatusOf(autoMode, ask) {
|
|
87
|
+
const breaker = classifierBreakerOf(autoMode);
|
|
88
|
+
// 🔴 历史记录**任何一态上都照带**(见模块顶注):被压下去的是**判词**,不是那条事实。
|
|
89
|
+
const withTrip = breaker === undefined ? {} : { breaker };
|
|
90
|
+
const round = ask === undefined ? undefined : classifierUnavailableOf(ask);
|
|
91
|
+
if (round !== undefined)
|
|
92
|
+
return { state: 'unavailable_this_round', cause: round.cause, ...withTrip };
|
|
93
|
+
const armed = typeof autoMode === 'object' && autoMode !== null && !Array.isArray(autoMode)
|
|
94
|
+
? autoMode.armed
|
|
95
|
+
: undefined;
|
|
96
|
+
// 🔴 `armed` 是**这条腿**的事实,压过账本里的历史记录 —— 同一会话重新武装之后分类器**真的在跑**。
|
|
97
|
+
if (armed === true)
|
|
98
|
+
return { state: 'available', ...withTrip };
|
|
99
|
+
if (breaker !== undefined)
|
|
100
|
+
return { state: 'breaker_open', breaker };
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 逐熔断成因一句人话(状态面用;与卡面那四句刻意不同 —— 见模块顶注)。
|
|
105
|
+
* 三句逐字互异:三条不同的下一步(去看模型那条腿 / 去调超时或换更快的分类模型 / 去看分类提示词与
|
|
106
|
+
* 契约)。
|
|
107
|
+
*/
|
|
108
|
+
const BREAKER_CAUSE_PHRASES = Object.freeze({
|
|
109
|
+
error: 'the classifier leg errored',
|
|
110
|
+
timeout: 'the classifier timed out',
|
|
111
|
+
parse_error: 'the classifier answered outside its contract',
|
|
112
|
+
});
|
|
113
|
+
/** 逐本轮成因一句人话(状态面用)。 */
|
|
114
|
+
const ROUND_CAUSE_PHRASES = Object.freeze({
|
|
115
|
+
error: 'the classifier leg errored',
|
|
116
|
+
timeout: 'the classifier timed out',
|
|
117
|
+
breaker_open: 'the session latch was already closed, so this round was never sent',
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* 一个成因词 → 一个短语。**按自有属性查表**(`Object.freeze` 不移除原型,裸下标会让一个来自 wire 的
|
|
121
|
+
* `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话)。
|
|
122
|
+
* 表外词 ⇒ 原样带上那个词供运维追问上游,**不冒充**任何一句已知的话。
|
|
123
|
+
*/
|
|
124
|
+
function phraseOf(table, cause) {
|
|
125
|
+
if (typeof cause === 'string' && Object.hasOwn(table, cause)) {
|
|
126
|
+
const hit = table[cause];
|
|
127
|
+
if (typeof hit === 'string')
|
|
128
|
+
return hit;
|
|
129
|
+
}
|
|
130
|
+
const word = typeof cause === 'string' && cause.length > 0 ? cause : '(none)';
|
|
131
|
+
return `it reported ${word}, a cause word newer than this client`;
|
|
132
|
+
}
|
|
133
|
+
/** epoch ms → UTC ISO(入参已由窄读收进 `Date` 值域,所以这里不会抛;见模块顶注)。 */
|
|
134
|
+
function utcIso(ms) {
|
|
135
|
+
return new Date(ms).toISOString();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 一次状态读数 → 一句人话。**唯一措辞铸点**(三端共用;端零自拼)。
|
|
139
|
+
*
|
|
140
|
+
* 三句逐字互异,且与 `classifierUnavailableDetail` 的四句**零重合**(两张面答两个问题)。
|
|
141
|
+
* 🔴 **表外态 / 熔断态记录读不出来都不抛**:型面挡不住 wire,也挡不住一份从持久态恢复回来的视图
|
|
142
|
+
* —— `{state:'breaker_open'}`(没有 `breaker`)、`breaker: null`、`openedAtMs: 1e20`(会让
|
|
143
|
+
* `toISOString()` 抛 `RangeError`)都必须渲出一句诚实的话,而不是把整屏带崩。本函数因此**自己
|
|
144
|
+
* 复核**那只记录(走同一只窄读),读不出就走「没被告知何时因何」那一句,绝不半渲一个假读数。
|
|
145
|
+
*/
|
|
146
|
+
export function classifierStatusDetail(view) {
|
|
147
|
+
const state = typeof view?.state === 'string' ? view.state : '';
|
|
148
|
+
if (state === 'available') {
|
|
149
|
+
// 🔴 「在跑」与「在跑、但这个会话上**曾经**熔断过」是**两条不同的下一步**:后者要去看那次失败
|
|
150
|
+
// 为什么发生(它随时可能再来一次),所以单出一句,而**不说**「不新开会话就恢复不了」——
|
|
151
|
+
// 那句话在一条已经重新武装的腿上是**假的**。
|
|
152
|
+
const past = classifierBreakerOf({ breaker: view.breaker });
|
|
153
|
+
if (past !== undefined) {
|
|
154
|
+
return (`the auto-mode classifier is running on this session, but it had latched off earlier — ` +
|
|
155
|
+
`${phraseOf(BREAKER_CAUSE_PHRASES, past.lastCause)} on ${past.failures} consecutive rounds, ` +
|
|
156
|
+
`and that latch closed at ${utcIso(past.openedAtMs)}`);
|
|
157
|
+
}
|
|
158
|
+
return 'the auto-mode classifier is running on this session';
|
|
159
|
+
}
|
|
160
|
+
if (state === 'breaker_open') {
|
|
161
|
+
// 🔴 **渲染入口自己复核那只记录**,不假定调用方一定经过 {@link classifierBreakerOf}
|
|
162
|
+
// (异源对抗复审 r1 finding④ 的真病)。`ClassifierBreakerView.openedAtMs` 在型面上只是
|
|
163
|
+
// `number` —— 一个宿主自建管线、或一份从持久态恢复回来的视图,造得出 `1e20`(`toISOString()`
|
|
164
|
+
// 当场 `RangeError`)与 `null`(`TypeError`),而那是**整屏崩**不是一行渲不出。
|
|
165
|
+
// 复核走的是**同一只窄读**(把它包回 `{breaker}` 的形喂进去),所以两处永远同一套判据 ——
|
|
166
|
+
// 在这里另写一遍范围检查就是第二份会漂的台账。
|
|
167
|
+
const b = classifierBreakerOf({ breaker: view.breaker });
|
|
168
|
+
if (b === undefined) {
|
|
169
|
+
return 'the auto-mode classifier is latched off for this session; this client was not told when it tripped or why';
|
|
170
|
+
}
|
|
171
|
+
// 🔴 **不说「不新开会话就恢复不了」**(异源对抗复审 r2):闩是**一条腿**的事实,同一会话的后一条
|
|
172
|
+
// 腿重新武装时会铸一只新的、闩关着的 decider ⇒ 那句话是编的。这里只说真的发生过的事。
|
|
173
|
+
return (`auto mode fell back to asking on this session — ${phraseOf(BREAKER_CAUSE_PHRASES, b.lastCause)} ` +
|
|
174
|
+
`on ${b.failures} consecutive rounds, and the latch closed at ${utcIso(b.openedAtMs)}`);
|
|
175
|
+
}
|
|
176
|
+
if (state === 'unavailable_this_round') {
|
|
177
|
+
return `the auto-mode classifier did not run this round — ${phraseOf(ROUND_CAUSE_PHRASES, view.cause)}`;
|
|
178
|
+
}
|
|
179
|
+
const word = state.length > 0 ? state : '(none)';
|
|
180
|
+
return `the auto-mode classifier reported ${word}, a state word newer than this client`;
|
|
181
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,8 @@ export * from './permissionRuleIssue.js';
|
|
|
160
160
|
export * from './toolRoster.js';
|
|
161
161
|
export * from './engineNoticeCodes.js';
|
|
162
162
|
export * from './autoModeUnavailable.js';
|
|
163
|
+
export * from './modelCapabilityProbe.js';
|
|
164
|
+
export * from './classifierStatus.js';
|
|
163
165
|
export * from './engineToolLabelStore.js';
|
|
164
166
|
export * from './fleetTaskDesc.js';
|
|
165
167
|
export type * from './types/engineState.js';
|
package/dist/index.js
CHANGED
|
@@ -195,6 +195,16 @@ export * from './engineNoticeCodes.js';
|
|
|
195
195
|
// 0.63.0 件⑧(core 7.10.0 #616):「这只 ask 是因为分类器跑不了才问人」的事实读器 + 措辞铸点。
|
|
196
196
|
// 两条 cause 轴刻意不合并(不可用 / 熔断),`parse_error` 只在熔断轴上 —— 读器按不可用轴收窄。
|
|
197
197
|
export * from './autoModeUnavailable.js';
|
|
198
|
+
// 0.64.0 件①(core 7.10.0 `OpenAICompletionsCompat.thinkingFormat`):一条 OpenAI-completions
|
|
199
|
+
// 车道的模型「会不会思考、关不关得掉、用哪种拼法关」的三端公共探测 + 落笔纯函数。
|
|
200
|
+
// 🔴 零 fetch:网络那半场经注入的 `ProbeSend` 端口,凭证一个字节都不经过本包(与
|
|
201
|
+
// `model/catalogLoader.ts` 的 `CatalogFetchJson` 同一条纪律)。试关序是**公面承诺**,门钉死。
|
|
202
|
+
export * from './modelCapabilityProbe.js';
|
|
203
|
+
// 0.64.0 件②(core 7.10.0 `AutoModeBreakerTrip`;server ≥7.69.0 才投那一段):auto 分类器的
|
|
204
|
+
// **状态面**读器与措辞铸点(诊断行 / 模型设置页 / 权限卡状态栏)。与 `autoModeUnavailable.ts` 的
|
|
205
|
+
// **卡面**那一问刻意分家:卡答「这一刻为什么问我」,状态答「这个会话上分类器现在是什么状态」。
|
|
206
|
+
// 🔴 `classifierBreakerOf` 是会话轴那一处的**唯一**窄读 —— `wiring_manifest` 投影臂共用它。
|
|
207
|
+
export * from './classifierStatus.js';
|
|
198
208
|
export * from './engineToolLabelStore.js';
|
|
199
209
|
export * from './fleetTaskDesc.js';
|
|
200
210
|
// ── B2 批:通知族合并 / caps-wire 门族 / 模型面纯逻辑 / control 路由(2026-07-27)──────────────
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/modelCapabilityProbe.ts — 一条 OpenAI-completions 车道的模型**会不会思考、关不关得掉**
|
|
3
|
+
* 的三端公共探测(0.64.0 件①)。
|
|
4
|
+
*
|
|
5
|
+
* -- 它答的是哪一问 ---------------------------------------------------------------------------
|
|
6
|
+
* 一台 vLLM/Qwen 类网关**默认就在思考**,而「关掉」这件事各家拼法不同:有的读顶层
|
|
7
|
+
* `enable_thinking`,有的只认模板参数 `chat_template_kwargs.enable_thinking`,有的要
|
|
8
|
+
* `thinking:{type:"disabled"}`。目录条目上那一格 `compat.thinkingFormat` 就是告诉引擎该用哪种
|
|
9
|
+
* 拼法 —— 而**没有人能靠看一眼型号名答出它是哪一种**。本模块把这一问变成一次可复现的探测:
|
|
10
|
+
* 发一发最小请求看它思不思考,思考就按序把每一种拼法试一遍,记下第一种真关得掉的。
|
|
11
|
+
*
|
|
12
|
+
* -- 🔴 网络那半场不在包里 --------------------------------------------------------------------
|
|
13
|
+
* 本模块**零 fetch、零 URL、零凭证**:调用方注入一只 {@link ProbeSend},自己去做鉴权与传输,
|
|
14
|
+
* 只把「答了多长的正文 / 有没有思考通道 / 什么 finish 位 / 什么状态码」交回来。理由与本包
|
|
15
|
+
* `model/catalogLoader.ts` 的 `CatalogFetchJson` 同一条:三端的凭证轨、代理、超时策略各不相同,
|
|
16
|
+
* 而**判定**是同一份 —— 判定归包,传输归端。凭证因此一个字节都不会经过本包。
|
|
17
|
+
*
|
|
18
|
+
* -- 🔴 方言闭集是**抄件**,不是本包的意见 ---------------------------------------------------
|
|
19
|
+
* 七个词的属主是引擎:`@sema-agent/core` 的 `dist/engine/llm/types.d.ts` 里
|
|
20
|
+
* `OpenAICompletionsCompat.thinkingFormat`(7.10.0 = 七词)。而 core **不是本包消费者的依赖**
|
|
21
|
+
* (既非 peer 也非 runtime dep),`.d.ts` 一旦引用它,装了本包却没装 core 的下游会当场编译不过
|
|
22
|
+
* ⇒ 与 `engineNoticeCodes.ts` / `autoModeUnavailable.ts` 同一条处置:**按真字节镜像,把代价交给
|
|
23
|
+
* 门** —— `run-model-capability-probe-test.mjs` 的 A 段对实装 devDep core 双向等值对账,
|
|
24
|
+
* 上游加一个方言这里当天先红。
|
|
25
|
+
* ⚠️ **settings-schema 那边取不到这张表,别去那儿找**:`ModelEntry.compat` 在 1.10.0 上是
|
|
26
|
+
* **parse-transparent 的 `z.unknown()`**,它的 JSDoc 逐字写着「the SHAPE and the VOCABULARY …
|
|
27
|
+
* are the consuming side's single point — this package deliberately does NOT restate that table」。
|
|
28
|
+
* 那一格刻意不复述词表,所以「从 settings-schema import 闭集」这条路在型面上不存在。
|
|
29
|
+
*
|
|
30
|
+
* -- 🔴 试关序是**公面承诺**,不是实现细节 ---------------------------------------------------
|
|
31
|
+
* 每多试一种拼法就是对那台网关多烧一次真钱与真时延,所以「先试哪一种」必须成文、必须被门钉死
|
|
32
|
+
* ({@link THINKING_DISABLE_PROBE_ORDER})。排序的理由是观测到的真实序列:一台 Qwen 类网关收到
|
|
33
|
+
* **顶层** `enable_thinking:false` 时,思考照旧、而正文变空 —— 换成模板参数才真的关掉。把模板形
|
|
34
|
+
* 排在前面,绝大多数这类网关一发即中。
|
|
35
|
+
* 🔴 **`openai` 不在试关序里**:它的「关」拼法是 `reasoning_effort:<thinkingLevelMap.off>`,而那张
|
|
36
|
+
* 映射表是**条目上的声明**,探针手里没有 ⇒ core 在这一形上什么都不写,发出去等于把 baseline
|
|
37
|
+
* 那一发原样再烧一次。它在本模块里的角色只有一个:关不掉时补一发 `reasoning_effort:"low"`,
|
|
38
|
+
* 看这台网关**收不收**低档旋钮({@link ModelProbeEvidence.lowEffortAccepted})。
|
|
39
|
+
* 🔴 **线上同形的两个词不各占一臂**:`zai` 与 `qwen` 铸出的请求体逐字节相同(顶层
|
|
40
|
+
* `enable_thinking`),`together` 与 `openrouter` 同理(`reasoning:{enabled:false}`)——
|
|
41
|
+
* 再烧一次往返证明不了任何新东西。对照表在 {@link THINKING_FORMAT_WIRE_TWINS};铸出的词取
|
|
42
|
+
* 每对里线上更常见的那一个,而**它们在这条轴上真的可以互换**(不是近似)。
|
|
43
|
+
*/
|
|
44
|
+
/** 一次探测请求的请求体 —— OpenAI-completions 车道的最小子集(方言位逐次至多一个在场)。 */
|
|
45
|
+
export interface OpenAICompletionsProbeBody {
|
|
46
|
+
model: string;
|
|
47
|
+
messages: ReadonlyArray<{
|
|
48
|
+
role: 'user';
|
|
49
|
+
content: string;
|
|
50
|
+
}>;
|
|
51
|
+
max_tokens: number;
|
|
52
|
+
stream: false;
|
|
53
|
+
/** `qwen` / `zai` 的关拼法。 */
|
|
54
|
+
enable_thinking?: boolean;
|
|
55
|
+
/** `qwen-chat-template` 的关拼法(模板渲染参数)。
|
|
56
|
+
* 🔴 **不是 `Record<string, unknown>`**:这只体**只由本包铸**(端拿到它就原样转发),所以这一位
|
|
57
|
+
* 上真正会出现的只有这一个开关 —— 声明成自由记录等于在公面上开一个没有理由的 unknown 出境口
|
|
58
|
+
* (typeshape 棘轮盯的正是这个),而且会让端以为自己可以往里塞别的模板参数。 */
|
|
59
|
+
chat_template_kwargs?: {
|
|
60
|
+
enable_thinking: boolean;
|
|
61
|
+
};
|
|
62
|
+
/** `deepseek` 的关拼法。 */
|
|
63
|
+
thinking?: {
|
|
64
|
+
type: string;
|
|
65
|
+
};
|
|
66
|
+
/** `openrouter` / `together` 的关拼法。 */
|
|
67
|
+
reasoning?: {
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
};
|
|
70
|
+
/** `openai` 车道的档位旋钮(本模块只用它做 low 档探测,**不**当作「关」)。 */
|
|
71
|
+
reasoning_effort?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 一次探测的**回执**(端负责把自己那条 HTTP 应答折成这个形)。
|
|
75
|
+
* 🔴 只要四件事,一件都不要更多:正文、思考通道、finish 位、状态码 —— 端不必、也不该把整只
|
|
76
|
+
* 应答体交进来(那会让本包变成第二个应答解析器,而解析归引擎)。
|
|
77
|
+
*/
|
|
78
|
+
export interface ProbeSendResult {
|
|
79
|
+
/** 模型答的正文(`choices[0].message.content`;没有 ⇒ 空串)。 */
|
|
80
|
+
content: string;
|
|
81
|
+
/** 思考通道(`choices[0].message.reasoning_content`);缺席 = 这台网关没给这一通道。 */
|
|
82
|
+
reasoningContent?: string;
|
|
83
|
+
/** `choices[0].finish_reason` 原样。 */
|
|
84
|
+
finishReason?: string;
|
|
85
|
+
/** HTTP 状态码。**缺席 = 端没报**,不是 200 —— 本模块因此只把「明确 ≥400」判成被拒。 */
|
|
86
|
+
status?: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 网络端口:把一只请求体发出去,交回 {@link ProbeSendResult}。
|
|
90
|
+
* 🔴 **抛错是合法回答**(端不必自己吞):本模块把「抛了」与「4xx/5xx」一视同仁地记成
|
|
91
|
+
* `refused`,并按发生在哪一发决定判词(见 {@link probeModelCapability})。
|
|
92
|
+
*/
|
|
93
|
+
export type ProbeSend = (body: OpenAICompletionsProbeBody) => Promise<ProbeSendResult>;
|
|
94
|
+
/** 被探测的目录条目(只要这三格 —— 条目的其余键与本探测无关)。 */
|
|
95
|
+
export interface ModelProbeEntry {
|
|
96
|
+
/** 线上模型 id(进请求体的那一个)。 */
|
|
97
|
+
id: string;
|
|
98
|
+
/** 车道(本模块只服务 `openai-completions`;别的车道由调用方先筛掉)。 */
|
|
99
|
+
api: string;
|
|
100
|
+
/** 端点(本模块**不读它**,只是让调用方的 `send` 闭包不必再查一次条目)。 */
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 引擎的**思考拼法闭集**(core `OpenAICompletionsCompat.thinkingFormat`,7.10.0 = 七词;
|
|
105
|
+
* 顺序同源)。🔴 这是一份抄件,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
|
|
106
|
+
|
|
107
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
108
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
109
|
+
*/
|
|
110
|
+
export declare const THINKING_FORMATS: readonly string[];
|
|
111
|
+
/**
|
|
112
|
+
* 线上**逐字节同形**的方言对(`键` ⇒ 探针实际会试的那一个)。它们在这条轴上可以互换:
|
|
113
|
+
* `zai` 与 `qwen` 都写顶层 `enable_thinking`,`together` 与 `openrouter` 都写 `reasoning.enabled`。
|
|
114
|
+
* ⚠️ 只对**思考开关**这一条轴成立 —— 别把它读成「这两个词处处等价」。
|
|
115
|
+
*/
|
|
116
|
+
export declare const THINKING_FORMAT_WIRE_TWINS: Readonly<Record<string, string>>;
|
|
117
|
+
/**
|
|
118
|
+
* **试关序**(公面承诺,见模块顶注)。四臂覆盖全部四种线上不同形的关拼法;`openai` 刻意不在其中。
|
|
119
|
+
* 🔴 顺序承重:模板形排第一,因为观测到的真实序列是「顶层形关不掉且把正文答空、模板形才真关掉」。
|
|
120
|
+
*/
|
|
121
|
+
export declare const THINKING_DISABLE_PROBE_ORDER: readonly string[];
|
|
122
|
+
/**
|
|
123
|
+
* 一次探测的四个判词。
|
|
124
|
+
* · `no_reasoning` —— 这台网关本来就不思考(一发即知,不再烧往返);
|
|
125
|
+
* · `disabled_via` —— 会思考,而 {@link ModelProbeResult.thinkingFormat} 那一种拼法真关掉了;
|
|
126
|
+
* · `cannot_disable` —— 会思考,四种拼法**逐臂给出正面反证**(每一臂都看见思考还开着)⇒ 关不掉;
|
|
127
|
+
* · `inconclusive` —— **问不出来**(baseline 没有证据力,或**任一**试关臂没有证据力)。
|
|
128
|
+
* 🔴 `cannot_disable` 与 `inconclusive` 的边界是本模块最要紧的一条,而它锚的是**证据力**
|
|
129
|
+
* (见 `forceOf`)而不是「有没有被拒」:一臂超时、或一臂答了一条「零思考 + 正文空」的合法空
|
|
130
|
+
* 回执,那一种拼法都**没被证伪**,唯一诚实的判词是「问不出来」。这一条不是洁癖:
|
|
131
|
+
* `cannot_disable` 是 {@link applyProbeToEntry} **删掉**条目上原有 `thinkingFormat` 的唯一触发,
|
|
132
|
+
* 而那一删是**持久**的。
|
|
133
|
+
*/
|
|
134
|
+
export declare const MODEL_PROBE_VERDICTS: readonly string[];
|
|
135
|
+
/** 一发的**可留痕部分** —— 长度与位,🔴 一个字节的应答正文都不进来(证据要能进日志与工单)。 */
|
|
136
|
+
export interface ModelProbeObservation {
|
|
137
|
+
/** 正文长度(码元数;**原始**长度,不因判据收紧而变 —— 证据面记的是读数)。 */
|
|
138
|
+
contentLen: number;
|
|
139
|
+
/**
|
|
140
|
+
* 正文里有没有**看得见的字**(去掉空白之后仍非空)。
|
|
141
|
+
* 🔴 与 {@link contentLen} 分开两格是刻意的:`{content:' '}` 的 `contentLen` 是 3(真读数),
|
|
142
|
+
* 而它在判据上**一个字都没说** —— 一格当两用会逼着判据去读一个它不该读的量。
|
|
143
|
+
*/
|
|
144
|
+
hasText: boolean;
|
|
145
|
+
/** 思考通道长度;没有该通道 ⇒ `0`。 */
|
|
146
|
+
reasoningLen: number;
|
|
147
|
+
/** 正文里出现了 `<think>` / `<thinking>` 开标签(没有思考通道的网关走这条)。 */
|
|
148
|
+
sawThinkTag: boolean;
|
|
149
|
+
/** `finish_reason` 原样(引擎/网关铸的词,不是用户内容)。 */
|
|
150
|
+
finishReason?: string;
|
|
151
|
+
/** 端报的状态码;缺席 = 端没报。 */
|
|
152
|
+
status?: number;
|
|
153
|
+
/** 这一发被拒了(`send` 抛错,或状态码 ≥400)。 */
|
|
154
|
+
refused: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** 一条试关臂的观测(比 {@link ModelProbeObservation} 多一格:试的是哪一种拼法)。 */
|
|
157
|
+
export interface ModelProbeAttempt extends ModelProbeObservation {
|
|
158
|
+
/** {@link THINKING_DISABLE_PROBE_ORDER} 之一。 */
|
|
159
|
+
format: string;
|
|
160
|
+
}
|
|
161
|
+
/** 一次探测留下的全部证据(零正文)。 */
|
|
162
|
+
export interface ModelProbeEvidence {
|
|
163
|
+
/** 第一发(不带任何方言位)。 */
|
|
164
|
+
baseline: ModelProbeObservation;
|
|
165
|
+
/** 逐臂试关(命中即停,所以这张表长度 ≤ 试关序长度)。 */
|
|
166
|
+
attempts: readonly ModelProbeAttempt[];
|
|
167
|
+
/**
|
|
168
|
+
* 关不掉之后补的那一发:这台网关**收不收** `reasoning_effort:"low"`。
|
|
169
|
+
* 🔴 它说的是「低档旋钮被收下了」,**不是**「思考真的变少了」—— 后者本探针量不到,别那样渲。
|
|
170
|
+
* 缺席 = 没做这一发(没到 `cannot_disable` 那一步)。
|
|
171
|
+
*/
|
|
172
|
+
lowEffortAccepted?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/** 一次探测的结论。 */
|
|
175
|
+
export interface ModelProbeResult {
|
|
176
|
+
/**
|
|
177
|
+
* 这台网关会不会思考。
|
|
178
|
+
* 🔴 **`inconclusive` 且 baseline 都没问出来时,这一格整个缺席** —— 不是 `false`。
|
|
179
|
+
* 把「问不出来」渲成「不会思考」正是本仓已定谳的病形([honest-absence-not-fabricated-zero]);
|
|
180
|
+
* 型面上让它可缺席,消费端就没法不小心把它读成一个读数。
|
|
181
|
+
*/
|
|
182
|
+
reasoning?: boolean;
|
|
183
|
+
/** 真关得掉的那一种拼法;🔴 只在 `disabled_via` 上在场。 */
|
|
184
|
+
thinkingFormat?: string;
|
|
185
|
+
/** {@link MODEL_PROBE_VERDICTS} 之一。 */
|
|
186
|
+
verdict: string;
|
|
187
|
+
evidence: ModelProbeEvidence;
|
|
188
|
+
}
|
|
189
|
+
/** 固定题面 —— 短、无歧义、任何模型都答得出,且答案长度可判。 */
|
|
190
|
+
export declare const PROBE_PROMPT = "Reply with exactly OK";
|
|
191
|
+
/** 输出上限:够答一句 `OK`,又不至于让一台在思考的网关烧掉一整个预算。 */
|
|
192
|
+
export declare const PROBE_MAX_TOKENS = 64;
|
|
193
|
+
/**
|
|
194
|
+
* 铸一只探测请求体。
|
|
195
|
+
* @param entry 被探测的条目(只取 `id`)
|
|
196
|
+
* @param format 关拼法;缺席 ⇒ baseline(不带任何方言位)
|
|
197
|
+
* @param opts `reasoningEffort` = low 档探测那一发(与关拼法互斥使用)
|
|
198
|
+
*
|
|
199
|
+
* 🔴 **逐次新对象**:调用方(与门)拿到的每一只都是新的,改一只不会污染下一只。
|
|
200
|
+
* 🔴 拼法逐字同 core `dist/brain/openai.js` 的 `applyThinking` 关分支;表外词 ⇒ 不写任何方言位
|
|
201
|
+
* (**不猜**:一个猜出来的旋钮会让判定去读一台网关根本没被要求过的行为)。
|
|
202
|
+
*/
|
|
203
|
+
export declare function probeRequestBody(entry: ModelProbeEntry, format?: string, opts?: {
|
|
204
|
+
reasoningEffort?: string;
|
|
205
|
+
}): OpenAICompletionsProbeBody;
|
|
206
|
+
/**
|
|
207
|
+
* 探测一个条目:会不会思考、关不关得掉、用哪种拼法关。
|
|
208
|
+
*
|
|
209
|
+
* 步骤(每一步都可能是最后一步 —— 能少烧一次往返就少烧一次):
|
|
210
|
+
* ① baseline:最小请求,不带任何方言位。
|
|
211
|
+
* · **没有证据力**(被拒,或「零思考 + 正文也空」的合法空回执)⇒ `inconclusive`,
|
|
212
|
+
* **结果里没有 `reasoning` 这一格**(问不出来 ≠ 不思考);
|
|
213
|
+
* · **正面证据**「正文非空且零思考」⇒ `no_reasoning`,收工(不再烧任何往返)。
|
|
214
|
+
* ② 按 {@link THINKING_DISABLE_PROBE_ORDER} 逐臂试关,第一个 {@link thinkingIsOff} 成立的
|
|
215
|
+
* 拼法就是答案 ⇒ `disabled_via`,收工。
|
|
216
|
+
* ③ 四臂都没关掉:
|
|
217
|
+
* · **每一臂都给出正面反证**(都看见思考还开着)⇒ `cannot_disable`,并补一发
|
|
218
|
+
* `reasoning_effort:"low"` 记下这台网关收不收低档旋钮(端据此渲「会思考、当前关不掉;
|
|
219
|
+
* 席位将按 low 档运行」);
|
|
220
|
+
* · **任一臂没有证据力**(被拒,或空回执)⇒ `inconclusive` —— 那一种拼法没被证伪,
|
|
221
|
+
* 「问不出来」不是「关不掉」。这一形上 `reasoning` 那一格**在场且为真**(baseline 是
|
|
222
|
+
* 问出来了的),而证据里逐臂留着每一发的长度与状态,端可以据此只重试那一臂。
|
|
223
|
+
*
|
|
224
|
+
* @param entry 被探测的条目
|
|
225
|
+
* @param send 网络端口(端自备鉴权与传输;本包零 fetch)
|
|
226
|
+
*/
|
|
227
|
+
export declare function probeModelCapability(entry: ModelProbeEntry, send: ProbeSend): Promise<ModelProbeResult>;
|
|
228
|
+
/** 一条目录条目上本模块会碰到的两键(其余键本模块一律不认识、也不动)。 */
|
|
229
|
+
type ProbeWritableEntry = {
|
|
230
|
+
reasoning?: boolean;
|
|
231
|
+
compat?: unknown;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* 把一次探测的结论落到条目上 —— **纯函数**:入参一个字节不动,返回一只新条目。
|
|
235
|
+
*
|
|
236
|
+
* 只写两键:`reasoning` 与 `compat.thinkingFormat`。条目上别的键(`name` / `tier` / `cost` /
|
|
237
|
+
* `extraBody` / `compat` 里别的位……)逐字原样带过去。
|
|
238
|
+
*
|
|
239
|
+
* 🔴 **`inconclusive` ⇒ 原样退回同一只条目**(连拷贝都不做,`===` 成立):问不出来的时候写什么
|
|
240
|
+
* 都是编。端要渲「这次没探到」就去读 `verdict`。
|
|
241
|
+
* 🔴 **`cannot_disable` ⇒ 把条目上原有的 `thinkingFormat` 删掉**,而不是留着不管。留着的后果是
|
|
242
|
+
* 双重的:引擎会继续发一种**已被实测证伪**的拼法,而目录/卡面会照那一格渲一句「思考已关」的
|
|
243
|
+
* 假话。删的只是那一键 —— `compat` 里别的位一个不动。
|
|
244
|
+
* 🔴 **`compat` 不是对象时不 spread**(wire 上什么都可能来):铸一只只带 `thinkingFormat` 的
|
|
245
|
+
* 干净新 `compat`,而不是把一段字符串摊进对象里。
|
|
246
|
+
*/
|
|
247
|
+
export declare function applyProbeToEntry<T extends ProbeWritableEntry>(entry: T, result: ModelProbeResult): T;
|
|
248
|
+
export {};
|