@sema-agent/client-core 0.63.2 → 0.64.0
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 +90 -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 +99 -0
- package/dist/classifierStatus.js +179 -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,291 @@
|
|
|
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
|
+
/**
|
|
45
|
+
* 引擎的**思考拼法闭集**(core `OpenAICompletionsCompat.thinkingFormat`,7.10.0 = 七词;
|
|
46
|
+
* 顺序同源)。🔴 这是一份抄件,不是本包的意见 —— 改它必须同 commit 附 core 坐标,且门会先红。
|
|
47
|
+
|
|
48
|
+
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
49
|
+
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
50
|
+
*/
|
|
51
|
+
export const THINKING_FORMATS = Object.freeze([
|
|
52
|
+
'openai',
|
|
53
|
+
'openrouter',
|
|
54
|
+
'deepseek',
|
|
55
|
+
'together',
|
|
56
|
+
'zai',
|
|
57
|
+
'qwen',
|
|
58
|
+
'qwen-chat-template',
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* 线上**逐字节同形**的方言对(`键` ⇒ 探针实际会试的那一个)。它们在这条轴上可以互换:
|
|
62
|
+
* `zai` 与 `qwen` 都写顶层 `enable_thinking`,`together` 与 `openrouter` 都写 `reasoning.enabled`。
|
|
63
|
+
* ⚠️ 只对**思考开关**这一条轴成立 —— 别把它读成「这两个词处处等价」。
|
|
64
|
+
*/
|
|
65
|
+
export const THINKING_FORMAT_WIRE_TWINS = Object.freeze({
|
|
66
|
+
zai: 'qwen',
|
|
67
|
+
together: 'openrouter',
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* **试关序**(公面承诺,见模块顶注)。四臂覆盖全部四种线上不同形的关拼法;`openai` 刻意不在其中。
|
|
71
|
+
* 🔴 顺序承重:模板形排第一,因为观测到的真实序列是「顶层形关不掉且把正文答空、模板形才真关掉」。
|
|
72
|
+
*/
|
|
73
|
+
export const THINKING_DISABLE_PROBE_ORDER = Object.freeze([
|
|
74
|
+
'qwen-chat-template',
|
|
75
|
+
'qwen',
|
|
76
|
+
'deepseek',
|
|
77
|
+
'openrouter',
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* 一次探测的四个判词。
|
|
81
|
+
* · `no_reasoning` —— 这台网关本来就不思考(一发即知,不再烧往返);
|
|
82
|
+
* · `disabled_via` —— 会思考,而 {@link ModelProbeResult.thinkingFormat} 那一种拼法真关掉了;
|
|
83
|
+
* · `cannot_disable` —— 会思考,四种拼法**逐臂给出正面反证**(每一臂都看见思考还开着)⇒ 关不掉;
|
|
84
|
+
* · `inconclusive` —— **问不出来**(baseline 没有证据力,或**任一**试关臂没有证据力)。
|
|
85
|
+
* 🔴 `cannot_disable` 与 `inconclusive` 的边界是本模块最要紧的一条,而它锚的是**证据力**
|
|
86
|
+
* (见 `forceOf`)而不是「有没有被拒」:一臂超时、或一臂答了一条「零思考 + 正文空」的合法空
|
|
87
|
+
* 回执,那一种拼法都**没被证伪**,唯一诚实的判词是「问不出来」。这一条不是洁癖:
|
|
88
|
+
* `cannot_disable` 是 {@link applyProbeToEntry} **删掉**条目上原有 `thinkingFormat` 的唯一触发,
|
|
89
|
+
* 而那一删是**持久**的。
|
|
90
|
+
*/
|
|
91
|
+
export const MODEL_PROBE_VERDICTS = Object.freeze([
|
|
92
|
+
'no_reasoning',
|
|
93
|
+
'disabled_via',
|
|
94
|
+
'cannot_disable',
|
|
95
|
+
'inconclusive',
|
|
96
|
+
]);
|
|
97
|
+
/** 固定题面 —— 短、无歧义、任何模型都答得出,且答案长度可判。 */
|
|
98
|
+
export const PROBE_PROMPT = 'Reply with exactly OK';
|
|
99
|
+
/** 输出上限:够答一句 `OK`,又不至于让一台在思考的网关烧掉一整个预算。 */
|
|
100
|
+
export const PROBE_MAX_TOKENS = 64;
|
|
101
|
+
/**
|
|
102
|
+
* 铸一只探测请求体。
|
|
103
|
+
* @param entry 被探测的条目(只取 `id`)
|
|
104
|
+
* @param format 关拼法;缺席 ⇒ baseline(不带任何方言位)
|
|
105
|
+
* @param opts `reasoningEffort` = low 档探测那一发(与关拼法互斥使用)
|
|
106
|
+
*
|
|
107
|
+
* 🔴 **逐次新对象**:调用方(与门)拿到的每一只都是新的,改一只不会污染下一只。
|
|
108
|
+
* 🔴 拼法逐字同 core `dist/brain/openai.js` 的 `applyThinking` 关分支;表外词 ⇒ 不写任何方言位
|
|
109
|
+
* (**不猜**:一个猜出来的旋钮会让判定去读一台网关根本没被要求过的行为)。
|
|
110
|
+
*/
|
|
111
|
+
export function probeRequestBody(entry, format, opts) {
|
|
112
|
+
const body = {
|
|
113
|
+
model: entry.id,
|
|
114
|
+
messages: [{ role: 'user', content: PROBE_PROMPT }],
|
|
115
|
+
max_tokens: PROBE_MAX_TOKENS,
|
|
116
|
+
stream: false,
|
|
117
|
+
};
|
|
118
|
+
switch (format) {
|
|
119
|
+
case 'qwen':
|
|
120
|
+
case 'zai':
|
|
121
|
+
body.enable_thinking = false;
|
|
122
|
+
break;
|
|
123
|
+
case 'qwen-chat-template':
|
|
124
|
+
body.chat_template_kwargs = { enable_thinking: false };
|
|
125
|
+
break;
|
|
126
|
+
case 'deepseek':
|
|
127
|
+
body.thinking = { type: 'disabled' };
|
|
128
|
+
break;
|
|
129
|
+
case 'openrouter':
|
|
130
|
+
case 'together':
|
|
131
|
+
body.reasoning = { enabled: false };
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (opts?.reasoningEffort !== undefined)
|
|
137
|
+
body.reasoning_effort = opts.reasoningEffort;
|
|
138
|
+
return body;
|
|
139
|
+
}
|
|
140
|
+
/** 正文里有没有思考开标签(没有 `reasoning_content` 通道的网关把思考写在正文里)。 */
|
|
141
|
+
function sawThinkTagIn(content) {
|
|
142
|
+
return content.includes('<think>') || content.includes('<thinking>');
|
|
143
|
+
}
|
|
144
|
+
/** 一发的观测(零正文)。`refused` = 抛错或状态码 ≥400。 */
|
|
145
|
+
function observe(r) {
|
|
146
|
+
const content = typeof r.content === 'string' ? r.content : '';
|
|
147
|
+
const reasoning = typeof r.reasoningContent === 'string' ? r.reasoningContent : '';
|
|
148
|
+
return {
|
|
149
|
+
contentLen: content.length,
|
|
150
|
+
hasText: content.trim().length > 0,
|
|
151
|
+
reasoningLen: reasoning.length,
|
|
152
|
+
sawThinkTag: sawThinkTagIn(content),
|
|
153
|
+
...(typeof r.finishReason === 'string' && r.finishReason.length > 0 ? { finishReason: r.finishReason } : {}),
|
|
154
|
+
...(typeof r.status === 'number' && Number.isFinite(r.status) ? { status: r.status } : {}),
|
|
155
|
+
refused: typeof r.status === 'number' && Number.isFinite(r.status) && r.status >= 400,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** 被拒的一发(`send` 抛了)——长度位如实为零,`refused` 为真。 */
|
|
159
|
+
function refusedObservation() {
|
|
160
|
+
return { contentLen: 0, hasText: false, reasoningLen: 0, sawThinkTag: false, refused: true };
|
|
161
|
+
}
|
|
162
|
+
/** 发一发,把抛错折成一次被拒的观测(🔴 探针答的是一个判词,不是一次崩)。 */
|
|
163
|
+
async function sendOnce(send, body) {
|
|
164
|
+
try {
|
|
165
|
+
return observe(await send(body));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// 🔴 刻意不留错误对象:它可能带 URL、请求头、乃至凭证片段([credential-error-path-leak]),
|
|
169
|
+
// 而本模块的证据面承诺过「可以整只进日志与工单」。归因靠端自己的传输日志。
|
|
170
|
+
return refusedObservation();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function forceOf(o) {
|
|
174
|
+
if (o.refused)
|
|
175
|
+
return 'moot';
|
|
176
|
+
// 思考的正面证据一到手就成立 —— 截断与否都不影响(见上面的 ⚠️)。
|
|
177
|
+
if (o.reasoningLen > 0 || o.sawThinkTag)
|
|
178
|
+
return 'on';
|
|
179
|
+
// 🔴 **正文空 / 纯空白 ⇒ moot,不是 off**:观测到的真实序列里,一台 Qwen 网关收到不认识的顶层
|
|
180
|
+
// 旋钮时会把思考留着、正文答空;而「零思考 + 正文空」同样出现在一次被截断的应答上。一个把话
|
|
181
|
+
// 答没了的旋钮不是「关掉了思考」—— 收它进来会让探针假阳,而假阳的代价是条目上落一个错的
|
|
182
|
+
// `thinkingFormat`,此后每一次真对话都发一种这台网关不认的拼法。
|
|
183
|
+
if (!o.hasText)
|
|
184
|
+
return 'moot';
|
|
185
|
+
// 🔴 **被截断 ⇒ moot**:`finish_reason: "length"` 说的是「这一发没答完」。本探针的 `max_tokens`
|
|
186
|
+
// 只有 64,一台在思考的网关很容易先烧完额度再被截掉 —— 那时思考通道可能一个字都没送出来。
|
|
187
|
+
// **按开集读**:只认上游明说的这一个词,别的词(含缺席 —— 端没报)一律不当截断,否则不报
|
|
188
|
+
// finish 位的宿主会永远拿不到 `off`。
|
|
189
|
+
if (o.finishReason === 'length')
|
|
190
|
+
return 'moot';
|
|
191
|
+
return 'off';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 探测一个条目:会不会思考、关不关得掉、用哪种拼法关。
|
|
195
|
+
*
|
|
196
|
+
* 步骤(每一步都可能是最后一步 —— 能少烧一次往返就少烧一次):
|
|
197
|
+
* ① baseline:最小请求,不带任何方言位。
|
|
198
|
+
* · **没有证据力**(被拒,或「零思考 + 正文也空」的合法空回执)⇒ `inconclusive`,
|
|
199
|
+
* **结果里没有 `reasoning` 这一格**(问不出来 ≠ 不思考);
|
|
200
|
+
* · **正面证据**「正文非空且零思考」⇒ `no_reasoning`,收工(不再烧任何往返)。
|
|
201
|
+
* ② 按 {@link THINKING_DISABLE_PROBE_ORDER} 逐臂试关,第一个 {@link thinkingIsOff} 成立的
|
|
202
|
+
* 拼法就是答案 ⇒ `disabled_via`,收工。
|
|
203
|
+
* ③ 四臂都没关掉:
|
|
204
|
+
* · **每一臂都给出正面反证**(都看见思考还开着)⇒ `cannot_disable`,并补一发
|
|
205
|
+
* `reasoning_effort:"low"` 记下这台网关收不收低档旋钮(端据此渲「会思考、当前关不掉;
|
|
206
|
+
* 席位将按 low 档运行」);
|
|
207
|
+
* · **任一臂没有证据力**(被拒,或空回执)⇒ `inconclusive` —— 那一种拼法没被证伪,
|
|
208
|
+
* 「问不出来」不是「关不掉」。这一形上 `reasoning` 那一格**在场且为真**(baseline 是
|
|
209
|
+
* 问出来了的),而证据里逐臂留着每一发的长度与状态,端可以据此只重试那一臂。
|
|
210
|
+
*
|
|
211
|
+
* @param entry 被探测的条目
|
|
212
|
+
* @param send 网络端口(端自备鉴权与传输;本包零 fetch)
|
|
213
|
+
*/
|
|
214
|
+
export async function probeModelCapability(entry, send) {
|
|
215
|
+
const baseline = await sendOnce(send, probeRequestBody(entry));
|
|
216
|
+
const baseForce = forceOf(baseline);
|
|
217
|
+
// 🔴 **`moot` 的 baseline 什么都不写**:被拒、或一条「零思考 + 正文空」的合法回执 —— 后者
|
|
218
|
+
// (`{content:'', finishReason:'length'}`)修前被判 `no_reasoning` 并把 `reasoning:false`
|
|
219
|
+
// 写进条目,等于用一次空回执覆盖一份原本正确的能力声明(异源复审 r1 finding② 的真病)。
|
|
220
|
+
if (baseForce === 'moot') {
|
|
221
|
+
return { verdict: 'inconclusive', evidence: { baseline, attempts: [] } };
|
|
222
|
+
}
|
|
223
|
+
if (baseForce === 'off') {
|
|
224
|
+
return { reasoning: false, verdict: 'no_reasoning', evidence: { baseline, attempts: [] } };
|
|
225
|
+
}
|
|
226
|
+
const attempts = [];
|
|
227
|
+
const forces = [];
|
|
228
|
+
for (const format of THINKING_DISABLE_PROBE_ORDER) {
|
|
229
|
+
const o = await sendOnce(send, probeRequestBody(entry, format));
|
|
230
|
+
attempts.push({ format, ...o });
|
|
231
|
+
const f = forceOf(o);
|
|
232
|
+
forces.push(f);
|
|
233
|
+
if (f === 'off') {
|
|
234
|
+
return { reasoning: true, thinkingFormat: format, verdict: 'disabled_via', evidence: { baseline, attempts } };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 🔴 「四种拼法都试过、都关不掉」这句话要成立,四臂就得**逐臂给出正面反证**(每一臂都真的看见
|
|
238
|
+
// 思考还开着)—— 不只是「没被拒」(异源复审 r1 finding① 的两个形):一臂超时,那一种拼法
|
|
239
|
+
// 压根没被证伪;四臂全是空回执,那四发一个字都没证明。这一条不是洁癖:`cannot_disable` 是
|
|
240
|
+
// `applyProbeToEntry` **删掉**条目上原有 `thinkingFormat` 的唯一触发,而那一删是**持久**的
|
|
241
|
+
// —— 让一次偶发的超时或空回执去抹掉一份正确配置,是本探针能犯的最贵的错。
|
|
242
|
+
if (!forces.every((f) => f === 'on')) {
|
|
243
|
+
return { reasoning: true, verdict: 'inconclusive', evidence: { baseline, attempts } };
|
|
244
|
+
}
|
|
245
|
+
const low = await sendOnce(send, probeRequestBody(entry, undefined, { reasoningEffort: 'low' }));
|
|
246
|
+
return {
|
|
247
|
+
reasoning: true,
|
|
248
|
+
verdict: 'cannot_disable',
|
|
249
|
+
evidence: { baseline, attempts, lowEffortAccepted: !low.refused },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 把一次探测的结论落到条目上 —— **纯函数**:入参一个字节不动,返回一只新条目。
|
|
254
|
+
*
|
|
255
|
+
* 只写两键:`reasoning` 与 `compat.thinkingFormat`。条目上别的键(`name` / `tier` / `cost` /
|
|
256
|
+
* `extraBody` / `compat` 里别的位……)逐字原样带过去。
|
|
257
|
+
*
|
|
258
|
+
* 🔴 **`inconclusive` ⇒ 原样退回同一只条目**(连拷贝都不做,`===` 成立):问不出来的时候写什么
|
|
259
|
+
* 都是编。端要渲「这次没探到」就去读 `verdict`。
|
|
260
|
+
* 🔴 **`cannot_disable` ⇒ 把条目上原有的 `thinkingFormat` 删掉**,而不是留着不管。留着的后果是
|
|
261
|
+
* 双重的:引擎会继续发一种**已被实测证伪**的拼法,而目录/卡面会照那一格渲一句「思考已关」的
|
|
262
|
+
* 假话。删的只是那一键 —— `compat` 里别的位一个不动。
|
|
263
|
+
* 🔴 **`compat` 不是对象时不 spread**(wire 上什么都可能来):铸一只只带 `thinkingFormat` 的
|
|
264
|
+
* 干净新 `compat`,而不是把一段字符串摊进对象里。
|
|
265
|
+
*/
|
|
266
|
+
export function applyProbeToEntry(entry, result) {
|
|
267
|
+
if (result.verdict === 'inconclusive')
|
|
268
|
+
return entry;
|
|
269
|
+
const compatIn = typeof entry.compat === 'object' && entry.compat !== null && !Array.isArray(entry.compat)
|
|
270
|
+
? entry.compat
|
|
271
|
+
: undefined;
|
|
272
|
+
if (result.verdict === 'no_reasoning') {
|
|
273
|
+
return { ...entry, reasoning: false };
|
|
274
|
+
}
|
|
275
|
+
if (result.verdict === 'disabled_via' && typeof result.thinkingFormat === 'string') {
|
|
276
|
+
return { ...entry, reasoning: true, compat: { ...compatIn, thinkingFormat: result.thinkingFormat } };
|
|
277
|
+
}
|
|
278
|
+
if (result.verdict === 'cannot_disable') {
|
|
279
|
+
if (compatIn === undefined)
|
|
280
|
+
return { ...entry, reasoning: true };
|
|
281
|
+
// 🔴 **不用 `rest[k] = v` 裸下标**(异源对抗复审 r1 采纳的真病):`compat` 是从配置/wire 来的,
|
|
282
|
+
// 而 `JSON.parse('{"__proto__":{}}')` 造得出一个**自有** `__proto__` 键 —— 裸下标赋值会命中
|
|
283
|
+
// `Object.prototype` 上的 setter:那一键被静默丢掉,**而新对象的原型被换掉**。
|
|
284
|
+
// `Object.fromEntries` 走 CreateDataProperty,建的是自有属性,两个后果都不会发生
|
|
285
|
+
// (同理,本文件别处的对象展开 `{...}` 也是安全的 —— 出问题的只有下标赋值这一种写法)。
|
|
286
|
+
const rest = Object.fromEntries(Object.entries(compatIn).filter(([k]) => k !== 'thinkingFormat'));
|
|
287
|
+
return { ...entry, reasoning: true, compat: rest };
|
|
288
|
+
}
|
|
289
|
+
// 表外判词(一个比这一端新的词)⇒ 什么都不写,与 `inconclusive` 同一条纪律。
|
|
290
|
+
return entry;
|
|
291
|
+
}
|