@sema-agent/settings-schema 1.9.0 → 1.10.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 CHANGED
@@ -6,6 +6,31 @@
6
6
  > `@sema-agent/registry-core`(更早还曾是 `@sema-ai/registry-core`)。本档 `0.19.0` 以下各节里的
7
7
  > 包名是**历史记录**,如实保留不追改。
8
8
 
9
+ ## 1.10.0 — 2026-09-09 — `models.models[].compat` 引擎兼容声明席(镜像 `OpenAICompletionsCompat`;新键=minor)
10
+
11
+ - 引擎 `OpenAICompletionsCompat`(五键 `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
12
+ `requiresReasoningContentOnAssistantMessages` / `supportsReasoningEffort`)早就能按模型发兼容声明,但 `ModelEntry`
13
+ 没有这个键 ⇒ 目录条目写了 `compat` 在 parse 期被 strip 剥掉,声明**根本到不了引擎**;`extraBody` 也穿不过(引擎的
14
+ OpenAI 保留键表拦下 `chat_template_kwargs` / `enable_thinking`)。1.10.0 起 `ModelEntry.compat?`
15
+ (`z.unknown().optional()`)。
16
+ - **三问**:①谁需要 —— vLLM / Qwen 类**缺省开思考**的网关(关思考只能靠 `chat_template_kwargs.enable_thinking=false`),
17
+ 以及任何要按模型钉 `max_tokens` 拼写 / reasoning 档位的部署;②谁受伤 —— 无:additive 可选键,缺席 = 键不铸 = 今日行为,
18
+ 存量配置零迁移;③补偿 —— 不加旋钮(不写就没有这个键)。
19
+ - **解析透明**,与 1.7.2 `source` / 1.9.0 `toolFaces` 同一条规矩:形与词表(`thinkingFormat` 有哪几个词)的校验归
20
+ 消费方摄入侧**单点**,本仓刻意不复述那张表 —— 本域整域 all-or-nothing,一次形/词拒会把整套模型目录抹掉(1.7.1 升级
21
+ 回归的形)。
22
+ - 唯一保留的解析拒 = 与兄弟字段 `extraBody` **同一只**自由形安全判据(secret 形键/值、访问器、循环、非 JSON、过深):
23
+ 1.10.0 把这条流水线从 `extraBody` 里抽成共用的 `vetFreeFormJson(v, field)`(getter-free 描述符走查 → 单次
24
+ `JSON.stringify` → 复扫的 clone 即持久值),两个字段同调一处,不留第二份判据。`extraBody` 的「必须是 plain JSON
25
+ 对象」形门是它自己的(它要被摊进请求体),行为逐字不变(消息文本与判定结果均未变)。深度上限仍是 `MAX_EXTRA_BODY_DEPTH`
26
+ (`./hash` 上的已发布导出,不改名 —— 改名是 BREAKING,收益纯属观感),其 JSDoc 已改写成「所有自由形字段共用的一只上限」。
27
+ - 红先:`test/model-compat-1.10.test.ts` 七格(①原样保留 ①b 五键全量 ②缺席不铸 ③坏形不整域默认 ④安全判据拒
28
+ ⑤`loadLocalConfig` 全链到 `effective.models.models[].compat` ⑤b `readEffectiveWire` wire 腿同样透传且不降级整域)。
29
+ - 消费方点名(能力显式表态制):**worker 服务**(投影/env/读面,随其 7.69.0)、**CLI**(探针写目录条目)、
30
+ **config-center 产品**(models 域透传)。
31
+ - 顺带:`package-lock.json` 的 `packages[""].version` 自 1.6.0 起停在 `1.5.0`(历次发版只改了顶层 `version`),
32
+ 本节一并对齐到 1.10.0。
33
+
9
34
  ## 1.9.0 — 2026-09-08 — `mcp.servers[].toolFaces` 覆盖层声明面(core 7.8.0 design/388 片 1 F7;新键=minor)
10
35
 
11
36
  - core 7.8.0 起 `McpServerSpec.toolFaces?: Record<remoteToolName, { family?, pathTarget?: {param, aliases?, access, skillScopeEligible?}, renderHints?, ruleFace? }>`
package/dist/hash.d.ts CHANGED
@@ -20,9 +20,13 @@ export declare const MAX_HASH_DEPTH = 200;
20
20
  * getVersion/getEffective (hard-DoS poison pill) — the two caps were each hardened in isolation but never
21
21
  * reconciled (R18). The real max wrapper is ~4–6; 32 is a deliberate ≫ margin. */
22
22
  export declare const MAX_HASH_WRAPPER_RESERVE = 32;
23
- /** Depth cap for a standalone `extraBody` value, reconciled with the hash cap MINUS the wrapper reserve so that
24
- * ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any real
25
- * extraBody (OpenAI-style request params nest <10 deep). */
23
+ /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
+ * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
+ * real free-form value (OpenAI-style request params nest <10 deep).
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
+ * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
+ * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
+ * (renaming it would be a BREAKING change for a purely cosmetic gain). */
26
30
  export declare const MAX_EXTRA_BODY_DEPTH: number;
27
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */
28
32
  export declare function stableStringify(v: unknown, depth?: number): string;
package/dist/hash.js CHANGED
@@ -20,9 +20,13 @@ export const MAX_HASH_DEPTH = 200;
20
20
  * getVersion/getEffective (hard-DoS poison pill) — the two caps were each hardened in isolation but never
21
21
  * reconciled (R18). The real max wrapper is ~4–6; 32 is a deliberate ≫ margin. */
22
22
  export const MAX_HASH_WRAPPER_RESERVE = 32;
23
- /** Depth cap for a standalone `extraBody` value, reconciled with the hash cap MINUS the wrapper reserve so that
24
- * ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any real
25
- * extraBody (OpenAI-style request params nest <10 deep). */
23
+ /** Depth cap for a standalone FREE-FORM config value, reconciled with the hash cap MINUS the wrapper reserve so
24
+ * that ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any
25
+ * real free-form value (OpenAI-style request params nest <10 deep).
26
+ * Scope: the ONE cap for EVERY free-form field vetted by `vetFreeFormJson` (`ModelEntry.extraBody` and, since
27
+ * 1.10.0, `ModelEntry.compat`) — they sit at the SAME wrapper depth (`{models:{models:[{…}]}}`), so one cap
28
+ * serves both. The `EXTRA_BODY` spelling is kept because this is a published export on the `./hash` subpath
29
+ * (renaming it would be a BREAKING change for a purely cosmetic gain). */
26
30
  export const MAX_EXTRA_BODY_DEPTH = MAX_HASH_DEPTH - MAX_HASH_WRAPPER_RESERVE;
27
31
  /** Deterministic JSON with sorted object keys (so key order never affects the hash). */
28
32
  export function stableStringify(v, depth = 0) {
package/dist/types.d.ts CHANGED
@@ -184,6 +184,24 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
184
184
  * through), e.g. {frequency_penalty:0.5, presence_penalty:0.3}. Non-secret model metadata — same class
185
185
  * as cost/tier. Shipped on /effective.models.models[].extraBody; the service merges it into the request. */
186
186
  extraBody: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, Record<string, unknown> | undefined, unknown>;
187
+ /** Per-model wire-compat declaration for the OpenAI-completions lane — mirrors the engine's
188
+ * `OpenAICompletionsCompat` (five keys: `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
189
+ * `requiresReasoningContentOnAssistantMessages` / `supportsReasoningEffort`). Real need: a vLLM/Qwen-class
190
+ * gateway that thinks by DEFAULT can only be turned off with `chat_template_kwargs.enable_thinking=false`,
191
+ * which `extraBody` cannot carry (the engine reserves those request keys) — and without this key a catalog
192
+ * entry that declares it is stripped at parse, so the declaration never reaches the engine at all.
193
+ *
194
+ * **Parse-transparent** (`z.unknown()`), same rule as `McpServerSpec.source` (1.7.2) and `.toolFaces` (1.9.0):
195
+ * the SHAPE and the VOCABULARY (which `thinkingFormat` words exist) are the consuming side's single point —
196
+ * this package deliberately does NOT restate that table. This domain parses all-or-nothing, so a shape/word
197
+ * rejection HERE would blank the entire model catalog over one bad key (the 1.7.1 upgrade regression). ABSENT
198
+ * = key not minted = today's behaviour, zero migration.
199
+ *
200
+ * The ONE rejection that remains is the free-form SAFETY judgment shared with `extraBody`
201
+ * ({@link vetFreeFormJson}): a secret-shaped key/value, an accessor, a cycle, non-JSON, or too deep to hash
202
+ * later. That is not shape validation — it is the boundary every free-form value must clear to enter the
203
+ * persisted config and `stableHash` (R18). */
204
+ compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
187
205
  /** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
188
206
  * (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
189
207
  quotaWeight: z.ZodDefault<z.ZodNumber>;
@@ -219,6 +237,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
219
237
  cacheWrite: number;
220
238
  } | undefined;
221
239
  extraBody?: Record<string, unknown> | undefined;
240
+ compat?: unknown;
222
241
  notes?: string | undefined;
223
242
  }, {
224
243
  id: string;
@@ -248,6 +267,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
248
267
  cacheWrite?: number | undefined;
249
268
  } | undefined;
250
269
  extraBody?: unknown;
270
+ compat?: unknown;
251
271
  quotaWeight?: number | undefined;
252
272
  enabled?: boolean | undefined;
253
273
  notes?: string | undefined;
@@ -281,6 +301,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
281
301
  cacheWrite: number;
282
302
  } | undefined;
283
303
  extraBody?: Record<string, unknown> | undefined;
304
+ compat?: unknown;
284
305
  notes?: string | undefined;
285
306
  }, {
286
307
  id: string;
@@ -310,6 +331,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
310
331
  cacheWrite?: number | undefined;
311
332
  } | undefined;
312
333
  extraBody?: unknown;
334
+ compat?: unknown;
313
335
  quotaWeight?: number | undefined;
314
336
  enabled?: boolean | undefined;
315
337
  notes?: string | undefined;
@@ -504,6 +526,24 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
504
526
  * through), e.g. {frequency_penalty:0.5, presence_penalty:0.3}. Non-secret model metadata — same class
505
527
  * as cost/tier. Shipped on /effective.models.models[].extraBody; the service merges it into the request. */
506
528
  extraBody: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, Record<string, unknown> | undefined, unknown>;
529
+ /** Per-model wire-compat declaration for the OpenAI-completions lane — mirrors the engine's
530
+ * `OpenAICompletionsCompat` (five keys: `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
531
+ * `requiresReasoningContentOnAssistantMessages` / `supportsReasoningEffort`). Real need: a vLLM/Qwen-class
532
+ * gateway that thinks by DEFAULT can only be turned off with `chat_template_kwargs.enable_thinking=false`,
533
+ * which `extraBody` cannot carry (the engine reserves those request keys) — and without this key a catalog
534
+ * entry that declares it is stripped at parse, so the declaration never reaches the engine at all.
535
+ *
536
+ * **Parse-transparent** (`z.unknown()`), same rule as `McpServerSpec.source` (1.7.2) and `.toolFaces` (1.9.0):
537
+ * the SHAPE and the VOCABULARY (which `thinkingFormat` words exist) are the consuming side's single point —
538
+ * this package deliberately does NOT restate that table. This domain parses all-or-nothing, so a shape/word
539
+ * rejection HERE would blank the entire model catalog over one bad key (the 1.7.1 upgrade regression). ABSENT
540
+ * = key not minted = today's behaviour, zero migration.
541
+ *
542
+ * The ONE rejection that remains is the free-form SAFETY judgment shared with `extraBody`
543
+ * ({@link vetFreeFormJson}): a secret-shaped key/value, an accessor, a cycle, non-JSON, or too deep to hash
544
+ * later. That is not shape validation — it is the boundary every free-form value must clear to enter the
545
+ * persisted config and `stableHash` (R18). */
546
+ compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
507
547
  /** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
508
548
  * (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
509
549
  quotaWeight: z.ZodDefault<z.ZodNumber>;
@@ -539,6 +579,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
539
579
  cacheWrite: number;
540
580
  } | undefined;
541
581
  extraBody?: Record<string, unknown> | undefined;
582
+ compat?: unknown;
542
583
  notes?: string | undefined;
543
584
  }, {
544
585
  id: string;
@@ -568,6 +609,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
568
609
  cacheWrite?: number | undefined;
569
610
  } | undefined;
570
611
  extraBody?: unknown;
612
+ compat?: unknown;
571
613
  quotaWeight?: number | undefined;
572
614
  enabled?: boolean | undefined;
573
615
  notes?: string | undefined;
@@ -601,6 +643,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
601
643
  cacheWrite: number;
602
644
  } | undefined;
603
645
  extraBody?: Record<string, unknown> | undefined;
646
+ compat?: unknown;
604
647
  notes?: string | undefined;
605
648
  }, {
606
649
  id: string;
@@ -630,6 +673,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
630
673
  cacheWrite?: number | undefined;
631
674
  } | undefined;
632
675
  extraBody?: unknown;
676
+ compat?: unknown;
633
677
  quotaWeight?: number | undefined;
634
678
  enabled?: boolean | undefined;
635
679
  notes?: string | undefined;
@@ -753,6 +797,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
753
797
  cacheWrite: number;
754
798
  } | undefined;
755
799
  extraBody?: Record<string, unknown> | undefined;
800
+ compat?: unknown;
756
801
  notes?: string | undefined;
757
802
  }[];
758
803
  roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
@@ -808,6 +853,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
808
853
  cacheWrite?: number | undefined;
809
854
  } | undefined;
810
855
  extraBody?: unknown;
856
+ compat?: unknown;
811
857
  quotaWeight?: number | undefined;
812
858
  enabled?: boolean | undefined;
813
859
  notes?: string | undefined;
@@ -865,6 +911,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
865
911
  cacheWrite: number;
866
912
  } | undefined;
867
913
  extraBody?: Record<string, unknown> | undefined;
914
+ compat?: unknown;
868
915
  notes?: string | undefined;
869
916
  }[];
870
917
  roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
@@ -920,6 +967,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
920
967
  cacheWrite?: number | undefined;
921
968
  } | undefined;
922
969
  extraBody?: unknown;
970
+ compat?: unknown;
923
971
  quotaWeight?: number | undefined;
924
972
  enabled?: boolean | undefined;
925
973
  notes?: string | undefined;
@@ -9399,6 +9447,24 @@ export declare const DOMAIN_SCHEMAS: {
9399
9447
  * through), e.g. {frequency_penalty:0.5, presence_penalty:0.3}. Non-secret model metadata — same class
9400
9448
  * as cost/tier. Shipped on /effective.models.models[].extraBody; the service merges it into the request. */
9401
9449
  extraBody: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, Record<string, unknown> | undefined, unknown>;
9450
+ /** Per-model wire-compat declaration for the OpenAI-completions lane — mirrors the engine's
9451
+ * `OpenAICompletionsCompat` (five keys: `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
9452
+ * `requiresReasoningContentOnAssistantMessages` / `supportsReasoningEffort`). Real need: a vLLM/Qwen-class
9453
+ * gateway that thinks by DEFAULT can only be turned off with `chat_template_kwargs.enable_thinking=false`,
9454
+ * which `extraBody` cannot carry (the engine reserves those request keys) — and without this key a catalog
9455
+ * entry that declares it is stripped at parse, so the declaration never reaches the engine at all.
9456
+ *
9457
+ * **Parse-transparent** (`z.unknown()`), same rule as `McpServerSpec.source` (1.7.2) and `.toolFaces` (1.9.0):
9458
+ * the SHAPE and the VOCABULARY (which `thinkingFormat` words exist) are the consuming side's single point —
9459
+ * this package deliberately does NOT restate that table. This domain parses all-or-nothing, so a shape/word
9460
+ * rejection HERE would blank the entire model catalog over one bad key (the 1.7.1 upgrade regression). ABSENT
9461
+ * = key not minted = today's behaviour, zero migration.
9462
+ *
9463
+ * The ONE rejection that remains is the free-form SAFETY judgment shared with `extraBody`
9464
+ * ({@link vetFreeFormJson}): a secret-shaped key/value, an accessor, a cycle, non-JSON, or too deep to hash
9465
+ * later. That is not shape validation — it is the boundary every free-form value must clear to enter the
9466
+ * persisted config and `stableHash` (R18). */
9467
+ compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
9402
9468
  /** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
9403
9469
  * (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
9404
9470
  quotaWeight: z.ZodDefault<z.ZodNumber>;
@@ -9434,6 +9500,7 @@ export declare const DOMAIN_SCHEMAS: {
9434
9500
  cacheWrite: number;
9435
9501
  } | undefined;
9436
9502
  extraBody?: Record<string, unknown> | undefined;
9503
+ compat?: unknown;
9437
9504
  notes?: string | undefined;
9438
9505
  }, {
9439
9506
  id: string;
@@ -9463,6 +9530,7 @@ export declare const DOMAIN_SCHEMAS: {
9463
9530
  cacheWrite?: number | undefined;
9464
9531
  } | undefined;
9465
9532
  extraBody?: unknown;
9533
+ compat?: unknown;
9466
9534
  quotaWeight?: number | undefined;
9467
9535
  enabled?: boolean | undefined;
9468
9536
  notes?: string | undefined;
@@ -9496,6 +9564,7 @@ export declare const DOMAIN_SCHEMAS: {
9496
9564
  cacheWrite: number;
9497
9565
  } | undefined;
9498
9566
  extraBody?: Record<string, unknown> | undefined;
9567
+ compat?: unknown;
9499
9568
  notes?: string | undefined;
9500
9569
  }, {
9501
9570
  id: string;
@@ -9525,6 +9594,7 @@ export declare const DOMAIN_SCHEMAS: {
9525
9594
  cacheWrite?: number | undefined;
9526
9595
  } | undefined;
9527
9596
  extraBody?: unknown;
9597
+ compat?: unknown;
9528
9598
  quotaWeight?: number | undefined;
9529
9599
  enabled?: boolean | undefined;
9530
9600
  notes?: string | undefined;
@@ -9648,6 +9718,7 @@ export declare const DOMAIN_SCHEMAS: {
9648
9718
  cacheWrite: number;
9649
9719
  } | undefined;
9650
9720
  extraBody?: Record<string, unknown> | undefined;
9721
+ compat?: unknown;
9651
9722
  notes?: string | undefined;
9652
9723
  }[];
9653
9724
  roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
@@ -9703,6 +9774,7 @@ export declare const DOMAIN_SCHEMAS: {
9703
9774
  cacheWrite?: number | undefined;
9704
9775
  } | undefined;
9705
9776
  extraBody?: unknown;
9777
+ compat?: unknown;
9706
9778
  quotaWeight?: number | undefined;
9707
9779
  enabled?: boolean | undefined;
9708
9780
  notes?: string | undefined;
@@ -9760,6 +9832,7 @@ export declare const DOMAIN_SCHEMAS: {
9760
9832
  cacheWrite: number;
9761
9833
  } | undefined;
9762
9834
  extraBody?: Record<string, unknown> | undefined;
9835
+ compat?: unknown;
9763
9836
  notes?: string | undefined;
9764
9837
  }[];
9765
9838
  roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
@@ -9815,6 +9888,7 @@ export declare const DOMAIN_SCHEMAS: {
9815
9888
  cacheWrite?: number | undefined;
9816
9889
  } | undefined;
9817
9890
  extraBody?: unknown;
9891
+ compat?: unknown;
9818
9892
  quotaWeight?: number | undefined;
9819
9893
  enabled?: boolean | undefined;
9820
9894
  notes?: string | undefined;
package/dist/types.js CHANGED
@@ -209,6 +209,40 @@ export function extraBodyUnsafe(v) {
209
209
  }
210
210
  return false;
211
211
  }
212
+ /** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`
213
+ * and `ModelEntry.compat` share this ONE function (1.10.0; before that it was inline in `extraBody` and a second
214
+ * free-form field would have copied it). The judgment is identical for every such field, so it lives in one place:
215
+ *
216
+ * 1. getter-free descriptor walk ({@link extraBodyUnsafe}) over the RAW value — never invokes a getter, and any
217
+ * reflective throw (a hostile Proxy trap could throw a secret-bearing message) collapses into a FIXED message;
218
+ * 2. a SINGLE `JSON.stringify` — its clone is re-scanned, so a Proxy that fakes its descriptors still can't ship
219
+ * a secret, and that same clone IS what we return to persist, so a non-idempotent `toJSON` cannot validate one
220
+ * serialization and persist another (the pre-R10 superRefine+transform stringified TWICE).
221
+ *
222
+ * `field` only NAMES the field in the message — it never changes the judgment. What this does NOT do is validate
223
+ * SHAPE: a field's shape/vocabulary is the consumer's single point (1.7.2 / 1.9.0 rule), and this domain parses
224
+ * all-or-nothing, so a shape rejection here would blank the whole catalog. */
225
+ function vetFreeFormJson(v, field) {
226
+ let rawUnsafe;
227
+ try {
228
+ rawUnsafe = extraBodyUnsafe(v);
229
+ }
230
+ catch {
231
+ return { ok: false, message: `${field} could not be validated (reflective access threw)` }; // R11
232
+ }
233
+ if (rawUnsafe)
234
+ return { ok: false, message: `${field}: a key/value is a secret shape, non-JSON (Infinity/function/Map), an accessor, cyclic, or too deep` };
235
+ let clone;
236
+ try {
237
+ clone = JSON.parse(JSON.stringify(v)); // the ONLY serialization — its result is what we scan AND persist
238
+ }
239
+ catch {
240
+ return { ok: false, message: `${field} is not JSON-serializable` };
241
+ }
242
+ if (extraBodyUnsafe(clone))
243
+ return { ok: false, message: `${field}: its serialized form carries a secret` };
244
+ return { ok: true, value: clone };
245
+ }
212
246
  /** A k8s resource quantity ("250m", "1", "0.5", "512Mi", "1Gi", "128M") — catches garbage at config-submit
213
247
  * rather than surfacing as a reconcile ApplyError at the apiserver (consistent with ENV_NAME/DURATION guards). */
214
248
  export const K8S_QUANTITY = z.string().regex(/^\d+(\.\d+)?(m|[kKMGTPE]i?)?$/, 'must be a k8s quantity like "250m", "512Mi", "1Gi"');
@@ -313,10 +347,9 @@ export const ModelEntry = z.object({
313
347
  extraBody: z
314
348
  .unknown()
315
349
  .optional()
316
- // ONE transform that validates AND canonicalizes with a SINGLE JSON.stringify — the scanned clone IS the
317
- // persisted value, so a non-idempotent getter/Proxy can't validate one serialization and persist another
318
- // (the prior superRefine+transform stringified TWICE — review R10). getter-free descriptor walk bounds size
319
- // BEFORE the stringify; the clone is re-scanned so a Proxy that fakes its descriptors still can't ship a secret.
350
+ // The SHAPE gate (plain JSON object) is extraBody's OWN — it is spread into a request body, so a non-object
351
+ // is meaningless here; the SAFETY judgment is the shared vetFreeFormJson pipeline (see its doc: getter-free
352
+ // walk → single stringify → re-scanned clone IS the persisted value).
320
353
  .transform((v, ctx) => {
321
354
  if (v === undefined)
322
355
  return undefined;
@@ -325,33 +358,42 @@ export const ModelEntry = z.object({
325
358
  ctx.addIssue({ code: z.ZodIssueCode.custom, message: "extraBody must be a plain JSON object" });
326
359
  return z.NEVER;
327
360
  }
328
- // wrap in try/catch: a hostile Proxy's getOwnPropertyDescriptor/ownKeys trap could THROW a secret-bearing
329
- // message — never surface it; treat any reflective failure as invalid with a FIXED message. R11.
330
- let rawUnsafe;
331
- try {
332
- rawUnsafe = extraBodyUnsafe(v);
333
- }
334
- catch {
335
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "extraBody could not be validated (reflective access threw)" });
361
+ const vetted = vetFreeFormJson(v, "extraBody");
362
+ if (!vetted.ok) {
363
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vetted.message });
336
364
  return z.NEVER;
337
365
  }
338
- if (rawUnsafe) {
339
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "extraBody: a key/value is a secret shape, non-JSON (Infinity/function/Map), an accessor, cyclic, or too deep" });
340
- return z.NEVER;
341
- }
342
- let clone;
343
- try {
344
- clone = JSON.parse(JSON.stringify(v)); // the ONLY serialization — its result is what we scan AND persist
345
- }
346
- catch {
347
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "extraBody is not JSON-serializable" });
348
- return z.NEVER;
349
- }
350
- if (extraBodyUnsafe(clone)) {
351
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "extraBody: its serialized form carries a secret" });
366
+ return vetted.value;
367
+ }),
368
+ /** Per-model wire-compat declaration for the OpenAI-completions lane — mirrors the engine's
369
+ * `OpenAICompletionsCompat` (five keys: `thinkingFormat` / `reasoningEffortLevels` / `maxTokensField` /
370
+ * `requiresReasoningContentOnAssistantMessages` / `supportsReasoningEffort`). Real need: a vLLM/Qwen-class
371
+ * gateway that thinks by DEFAULT can only be turned off with `chat_template_kwargs.enable_thinking=false`,
372
+ * which `extraBody` cannot carry (the engine reserves those request keys) — and without this key a catalog
373
+ * entry that declares it is stripped at parse, so the declaration never reaches the engine at all.
374
+ *
375
+ * **Parse-transparent** (`z.unknown()`), same rule as `McpServerSpec.source` (1.7.2) and `.toolFaces` (1.9.0):
376
+ * the SHAPE and the VOCABULARY (which `thinkingFormat` words exist) are the consuming side's single point —
377
+ * this package deliberately does NOT restate that table. This domain parses all-or-nothing, so a shape/word
378
+ * rejection HERE would blank the entire model catalog over one bad key (the 1.7.1 upgrade regression). ABSENT
379
+ * = key not minted = today's behaviour, zero migration.
380
+ *
381
+ * The ONE rejection that remains is the free-form SAFETY judgment shared with `extraBody`
382
+ * ({@link vetFreeFormJson}): a secret-shaped key/value, an accessor, a cycle, non-JSON, or too deep to hash
383
+ * later. That is not shape validation — it is the boundary every free-form value must clear to enter the
384
+ * persisted config and `stableHash` (R18). */
385
+ compat: z
386
+ .unknown()
387
+ .optional()
388
+ .transform((v, ctx) => {
389
+ if (v === undefined)
390
+ return undefined;
391
+ const vetted = vetFreeFormJson(v, "compat");
392
+ if (!vetted.ok) {
393
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: vetted.message });
352
394
  return z.NEVER;
353
395
  }
354
- return clone;
396
+ return vetted.value;
355
397
  }),
356
398
  /** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
357
399
  * (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",