pi-openai-toolkit 0.20.8 → 0.21.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/src/config/v2.ts CHANGED
@@ -1,1380 +1,1167 @@
1
- /**
2
- * Version 2 global Toolkit configuration: validation, known-field merge and per-leaf
3
- * provenance.
4
- *
5
- * This module is a pure decoder. It performs no file I/O, no path resolution, no auth or
6
- * provider lookup and no tool mutation, and it deliberately depends on no JSON Schema
7
- * library. The shipped `config.schema.json` mirrors the accepted document, and the runtime
8
- * checks in this file stay the authority.
9
- *
10
- * Document shape (`schemaVersion` must be exactly 2):
11
- *
12
- * ```jsonc
13
- * {
14
- * "schemaVersion": 2,
15
- * "$schema": "…metadata only…",
16
- * "defaults": { "context": …, "webSearch": …, "imageGeneration": …, "responsesWebSocket": …, "autoMode": … },
17
- * "models": { "provider/model-id": { "context": …, "webSearch": …, "autoMode": …, "compatibility": … } },
18
- * "diagnostics": { … }
19
- * }
20
- * ```
21
- *
22
- * Rules:
23
- * - Precedence is built-ins -> `defaults` -> the exact override of the resolved model.
24
- * - Only known nested objects merge by field; arrays replace; `false` and `0` stay meaningful.
25
- * - `null` is accepted only for the optional producer/reviewer/classifier model references and
26
- * clears an inherited reference. `null` anywhere else is a path-specific error and never
27
- * deletes an object.
28
- * - Unknown properties are errors with masked names. Model-key diagnostic segments are bounded;
29
- * raw values and unknown subtrees are never copied into diagnostics.
30
- * - `defaults.compatibility` is not a recognized scope: the standard transport is internal and
31
- * the Codex gateway transport stays an exact-model opt-in.
32
- * - Image output settings (`defaults.imageGeneration`), Responses WebSocket transport
33
- * (`defaults.responsesWebSocket`), and `diagnostics` are global only.
34
- * - Exact model keys and model references use the legacy `provider/model-id` grammar: no
35
- * whitespace, globs or bracket patterns. Keys are trimmed like the legacy decoder, and two
36
- * raw keys that normalize to the same key are rejected instead of resolved last-wins.
37
- *
38
- * Fail-closed contract for consumers: `policy` always carries the best known value for every
39
- * leaf, so an invalid present value keeps the inherited or built-in value instead of silently
40
- * falling through to another route. The blocking signal is `invalidFeatures`, which is scoped to
41
- * the resolved model: a leaf error in `defaults` marks the owning feature for every model, a
42
- * leaf error in the resolved model's override marks that feature for that model only, an error
43
- * in a different model is reported without invalidating the current selection, and a valid exact
44
- * override shadows an invalid `defaults` leaf. Malformed document sections and unknown
45
- * properties inside a feature scope are feature-level errors that no override can mask.
46
- * `origins` explains the effective value of every leaf, so an ignored invalid value never claims
47
- * provenance it does not own.
48
- *
49
- * Unversioned documents belong to the legacy adapter and must not be routed here: a missing or
50
- * non-2 `schemaVersion`, a mixed legacy/v2 root and a non-object root are fatal document errors
51
- * that resolve the shipped defaults and mark every feature invalid.
52
- */
53
-
54
- import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
55
- import { MAX_IMAGE_MODEL_ID_CHARS } from "../image-generation/types";
56
- import {
57
- BREAKER_LIMIT_MAX,
58
- BREAKER_LIMIT_MIN,
59
- BREAKER_WINDOW_MAX,
60
- BREAKER_WINDOW_MIN,
61
- CLASSIFIER_MAX_LAG_MAX,
62
- CLASSIFIER_MAX_LAG_MIN,
63
- EVIDENCE_ROUNDS_MAX,
64
- EVIDENCE_ROUNDS_MIN,
65
- RESPONSES_COMPACT_CAPABLE_APIS,
66
- REVIEWER_TIMEOUT_MAX_MS,
67
- REVIEWER_TIMEOUT_MIN_MS,
68
- THINKING_LEVELS,
69
- } from "../types";
70
- import {
71
- CONFIG_FEATURES,
72
- createPolicyDefaults,
73
- type ConfigFeature,
74
- type ConfigIssue,
75
- type ConfigOrigin,
76
- type EffectiveToolkitPolicy,
77
- type ResolvedToolkitPolicy,
78
- type SearchRoute,
79
- } from "./policy";
80
-
81
- /**
82
- * Stable, controlled diagnostic codes. Adding a code is a compatible change; consumers may
83
- * match on these strings but must not build messages from raw configuration values.
84
- */
85
- export const V2_ISSUE = {
86
- rootInvalid: "document.root.invalid",
87
- schemaVersionMissing: "document.schemaVersion.missing",
88
- schemaVersionUnsupported: "document.schemaVersion.unsupported",
89
- formatMixed: "document.format.mixed",
90
- schemaInvalid: "document.$schema.invalid",
91
- defaultsInvalid: "document.defaults.invalid",
92
- modelsInvalid: "document.models.invalid",
93
- diagnosticsInvalid: "document.diagnostics.invalid",
94
- scopeInvalid: "scope.invalid",
95
- keyUnknown: "key.unknown",
96
- keyInvalid: "models.key.invalid",
97
- keyDuplicate: "models.key.duplicate",
98
- expectedBoolean: "value.expected.boolean",
99
- expectedString: "value.expected.string",
100
- expectedList: "value.expected.list",
101
- expectedInteger: "value.expected.integer",
102
- expectedEnum: "value.expected.enum",
103
- expectedModel: "value.expected.model",
104
- nullUnsupported: "value.null.unsupported",
105
- range: "value.range",
106
- length: "value.length",
107
- empty: "value.empty",
108
- apiUnsupported: "value.api.unsupported",
109
- imageDefaultNotAllowed: "imageGeneration.defaultModel.notAllowed",
110
- } as const;
111
-
112
- const CONTEXT_MODES = ["pi", "remote-compaction", "remote-windows"] as const;
113
- const INPUT_SOURCES = ["legacy", "pi-context-hook"] as const;
114
- const LEAVE_MANAGED_MODES = ["warn", "compact"] as const;
115
- const SEARCH_ROUTES = ["unmanaged", "local", "hosted", "standalone-alpha"] as const;
116
- const AUTO_GATES = ["side-effect", "all"] as const;
117
- const TRANSPORTS = ["standard", "codex-gateway"] as const;
118
- const DIAGNOSTIC_LEVELS = ["error", "warn", "info", "debug"] as const;
119
-
120
- /** v1 root sections; their presence next to `schemaVersion: 2` is a mixed-format document. */
121
- const LEGACY_ROOT_KEYS = ["compaction", "webSearch", "imageGeneration", "autoMode"] as const;
122
-
123
- const ROOT_KEYS = new Set(["$schema", "schemaVersion", "defaults", "models", "diagnostics"]);
124
- const DEFAULTS_KEYS = new Set(["context", "webSearch", "imageGeneration", "responsesWebSocket", "autoMode"]);
125
- const MODEL_OVERRIDE_KEYS = new Set(["context", "webSearch", "autoMode", "compatibility"]);
126
- const CONTEXT_KEYS = new Set(["mode", "remoteCompaction", "nativeFallback", "remoteWindows"]);
127
- const REMOTE_COMPACTION_KEYS = new Set(["model", "inputSource", "allowContinuityBreak", "apis"]);
128
- const NATIVE_FALLBACK_KEYS = new Set(["enabled", "model", "thinkingLevel"]);
129
- const REMOTE_WINDOWS_KEYS = new Set(["leaveManagedMode", "reminderThresholdPercent"]);
130
- const WEB_SEARCH_KEYS = new Set(["route"]);
131
- const IMAGE_GENERATION_KEYS = new Set(["enabled", "defaultModel", "allowedModels"]);
132
- const RESPONSES_WEBSOCKET_KEYS = new Set(["enabled"]);
133
- const AUTO_MODE_KEYS = new Set([
134
- "available",
135
- "reviewerModel",
136
- "gate",
137
- "extraTools",
138
- "timeoutMs",
139
- "transcript",
140
- "evidenceTools",
141
- "maxEvidenceRounds",
142
- "classifier",
143
- "circuitBreaker",
144
- ]);
145
- const CLASSIFIER_KEYS = new Set(["enabled", "model", "timeoutMs", "maxLag"]);
146
- const BREAKER_KEYS = new Set(["consecutiveDenials", "recentDenials", "windowSize"]);
147
- const COMPATIBILITY_KEYS = new Set(["transport"]);
148
- const DIAGNOSTICS_KEYS = new Set([
149
- "level",
150
- "notifyOnLoad",
151
- "captureRequests",
152
- "captureResponses",
153
- "redactSensitiveData",
154
- "artifactRoot",
155
- ]);
156
-
157
- /** Features a `models[exactKey]` entry is able to own. */
158
- const MODEL_OVERRIDABLE_FEATURES: readonly ConfigFeature[] = [
159
- "context",
160
- "webSearch",
161
- "autoMode",
162
- "compatibility",
163
- ];
164
-
165
- /**
166
- * Feature names usable as unknown-property attribution. `compaction` is the legacy name of the
167
- * context section, so a v2 document that still uses it fails the owning feature closed instead
168
- * of dropping the intent.
169
- */
170
- const FEATURE_NAMES: ReadonlyMap<string, ConfigFeature> = new Map<string, ConfigFeature>([
171
- ["context", "context"],
172
- ["compaction", "context"],
173
- ["webSearch", "webSearch"],
174
- ["imageGeneration", "imageGeneration"],
175
- ["responsesWebSocket", "responsesWebSocket"],
176
- ["autoMode", "autoMode"],
177
- ["compatibility", "compatibility"],
178
- ["diagnostics", "diagnostics"],
179
- ]);
180
-
181
- /**
182
- * Every effective leaf of `EffectiveToolkitPolicy`. Resolution fills `origins` for exactly these
183
- * paths, so a read-only explanation can account for every value that reaches a consumer.
184
- */
185
- const POLICY_LEAVES: readonly { leaf: string; feature: ConfigFeature }[] = [
186
- { leaf: "context.mode", feature: "context" },
187
- { leaf: "context.remoteCompaction.model", feature: "context" },
188
- { leaf: "context.remoteCompaction.inputSource", feature: "context" },
189
- { leaf: "context.remoteCompaction.allowContinuityBreak", feature: "context" },
190
- { leaf: "context.remoteCompaction.apis", feature: "context" },
191
- { leaf: "context.nativeFallback.enabled", feature: "context" },
192
- { leaf: "context.nativeFallback.model", feature: "context" },
193
- { leaf: "context.nativeFallback.thinkingLevel", feature: "context" },
194
- { leaf: "context.remoteWindows.leaveManagedMode", feature: "context" },
195
- { leaf: "context.remoteWindows.reminderThresholdPercent", feature: "context" },
196
- { leaf: "webSearch.route", feature: "webSearch" },
197
- { leaf: "imageGeneration.enabled", feature: "imageGeneration" },
198
- { leaf: "imageGeneration.defaultModel", feature: "imageGeneration" },
199
- { leaf: "imageGeneration.allowedModels", feature: "imageGeneration" },
200
- { leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
201
- { leaf: "autoMode.available", feature: "autoMode" },
202
- { leaf: "autoMode.reviewerModel", feature: "autoMode" },
203
- { leaf: "autoMode.gate", feature: "autoMode" },
204
- { leaf: "autoMode.extraTools", feature: "autoMode" },
205
- { leaf: "autoMode.timeoutMs", feature: "autoMode" },
206
- { leaf: "autoMode.transcript", feature: "autoMode" },
207
- { leaf: "autoMode.evidenceTools", feature: "autoMode" },
208
- { leaf: "autoMode.maxEvidenceRounds", feature: "autoMode" },
209
- { leaf: "autoMode.classifier.enabled", feature: "autoMode" },
210
- { leaf: "autoMode.classifier.model", feature: "autoMode" },
211
- { leaf: "autoMode.classifier.timeoutMs", feature: "autoMode" },
212
- { leaf: "autoMode.classifier.maxLag", feature: "autoMode" },
213
- { leaf: "autoMode.circuitBreaker.consecutiveDenials", feature: "autoMode" },
214
- { leaf: "autoMode.circuitBreaker.recentDenials", feature: "autoMode" },
215
- { leaf: "autoMode.circuitBreaker.windowSize", feature: "autoMode" },
216
- { leaf: "compatibility.transport", feature: "compatibility" },
217
- { leaf: "diagnostics.level", feature: "diagnostics" },
218
- { leaf: "diagnostics.notifyOnLoad", feature: "diagnostics" },
219
- { leaf: "diagnostics.captureRequests", feature: "diagnostics" },
220
- { leaf: "diagnostics.captureResponses", feature: "diagnostics" },
221
- { leaf: "diagnostics.redactSensitiveData", feature: "diagnostics" },
222
- { leaf: "diagnostics.artifactRoot", feature: "diagnostics" },
223
- ];
224
-
225
- /** Longest unrecognized key echoed into a diagnostic path. */
226
- const MAX_DIAGNOSTIC_SEGMENT_CHARS = 64;
227
- const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
228
- /** Characters that make a model key a pattern rather than an exact key. */
229
- const MODEL_KEY_PATTERN_CHARACTERS = /[\s*?[\]{}]/;
230
-
231
- function isRecord(value: unknown): value is Record<string, unknown> {
232
- return !!value && typeof value === "object" && !Array.isArray(value);
233
- }
234
-
235
- /**
236
- * Exact `provider/model-id` grammar shared with the legacy decoder: one separator, non-empty
237
- * halves, no whitespace and no glob/bracket characters.
238
- */
239
- function isExactModelKey(value: string): boolean {
240
- const separator = value.indexOf("/");
241
- if (separator <= 0 || separator >= value.length - 1) return false;
242
- return !MODEL_KEY_PATTERN_CHARACTERS.test(value) && !/[\u0000-\u001f\u007f]/.test(value);
243
- }
244
-
245
- /** Bound an unrecognized key before it reaches a diagnostic path; the text is never a value. */
246
- function boundedSegment(value: string): string {
247
- const cleaned = value.replace(CONTROL_CHARACTERS, "");
248
- return cleaned.length > MAX_DIAGNOSTIC_SEGMENT_CHARS
249
- ? `${cleaned.slice(0, MAX_DIAGNOSTIC_SEGMENT_CHARS)}…`
250
- : cleaned;
251
- }
252
-
253
- function joinPath(prefix: string, segment: string): string {
254
- return prefix ? `${prefix}.${segment}` : segment;
255
- }
256
-
257
- /** Input JSON path of a model scope, e.g. `models["openai/gpt-5.2"]`. */
258
- function modelScopePath(key: string): string {
259
- return `models[${JSON.stringify(key)}]`;
260
- }
261
-
262
- // ---------------------------------------------------------------------------------------------
263
- // Document scopes
264
- // ---------------------------------------------------------------------------------------------
265
-
266
- type ScopeKind = "document" | "defaults" | "model";
267
-
268
- type Scope = {
269
- /** JSON path of the scope object inside the user document; `""` is the document root. */
270
- input: string;
271
- kind: ScopeKind;
272
- /** Normalized exact key when `kind` is `"model"`. */
273
- modelKey?: string;
274
- };
275
-
276
- const DOCUMENT_SCOPE: Scope = { input: "", kind: "document" };
277
-
278
- function subScope(scope: Scope, key: string): Scope {
279
- return { ...scope, input: joinPath(scope.input, key) };
280
- }
281
-
282
- type FieldDescriptor = {
283
- /** Key inside the owning JSON object. */
284
- key: string;
285
- /** Effective policy leaf path, matching an entry in `POLICY_LEAVES`. */
286
- leaf: string;
287
- feature: ConfigFeature;
288
- };
289
-
290
- type LeafState = {
291
- feature: ConfigFeature;
292
- /** A present `defaults` value for this leaf failed validation. */
293
- invalidAtDefaults: boolean;
294
- /** A present override of the resolved model for this leaf failed validation. */
295
- invalidAtModel: boolean;
296
- /** A valid override of the resolved model supplied this leaf. */
297
- modelApplied: boolean;
298
- };
299
-
300
- type ResolutionState = {
301
- policy: EffectiveToolkitPolicy;
302
- origins: Record<string, ConfigOrigin>;
303
- issues: ConfigIssue[];
304
- leaves: Map<string, LeafState>;
305
- /** Feature-level errors a valid exact override cannot mask. */
306
- hardInvalid: Set<ConfigFeature>;
307
- /** Fatal document error: nothing is trusted and every feature is invalid. */
308
- untrusted: boolean;
309
- selectedKey: string | undefined;
310
- gatewayCandidates: Set<string>;
311
- };
312
-
313
- function createState(modelKey: string | undefined): ResolutionState {
314
- const origins: Record<string, ConfigOrigin> = {};
315
- for (const { leaf } of POLICY_LEAVES) origins[leaf] = { kind: "builtin" };
316
- const trimmed = typeof modelKey === "string" ? modelKey.trim() : "";
317
- return {
318
- policy: createPolicyDefaults(),
319
- origins,
320
- issues: [],
321
- leaves: new Map(),
322
- hardInvalid: new Set(),
323
- gatewayCandidates: new Set(),
324
- untrusted: false,
325
- selectedKey: trimmed.length > 0 ? trimmed : undefined,
326
- };
327
- }
328
-
329
- /** `defaults` and `diagnostics` always reach the resolved policy; a model scope only matches itself. */
330
- function scopeApplies(state: ResolutionState, scope: Scope): boolean {
331
- if (scope.kind === "model") return scope.modelKey === state.selectedKey;
332
- return true;
333
- }
334
-
335
- function leafState(state: ResolutionState, leaf: string, feature: ConfigFeature): LeafState {
336
- let entry = state.leaves.get(leaf);
337
- if (!entry) {
338
- entry = { feature, invalidAtDefaults: false, invalidAtModel: false, modelApplied: false };
339
- state.leaves.set(leaf, entry);
340
- }
341
- return entry;
342
- }
343
-
344
- function withModel(scope: Scope, issue: ConfigIssue): ConfigIssue {
345
- return scope.modelKey === undefined ? issue : { ...issue, modelKey: scope.modelKey };
346
- }
347
-
348
- // ---------------------------------------------------------------------------------------------
349
- // Diagnostics
350
- // ---------------------------------------------------------------------------------------------
351
-
352
- function fatalDocumentError(state: ResolutionState, code: string, path: string): void {
353
- state.issues.push({ severity: "error", code, path, feature: "document" });
354
- state.untrusted = true;
355
- }
356
-
357
- /** A value that is present but not usable for the leaf it was written to. */
358
- function reportLeafError(
359
- state: ResolutionState,
360
- scope: Scope,
361
- field: FieldDescriptor,
362
- code: string,
363
- inputPath: string,
364
- ): void {
365
- state.issues.push(
366
- withModel(scope, { severity: "error", code, path: inputPath, feature: field.feature }),
367
- );
368
- if (!scopeApplies(state, scope)) return;
369
- const leaf = leafState(state, field.leaf, field.feature);
370
- if (scope.kind === "model") leaf.invalidAtModel = true;
371
- else leaf.invalidAtDefaults = true;
372
- }
373
-
374
- /** A malformed object scope: the feature it owns cannot be trusted, override or not. */
375
- function reportScopeError(
376
- state: ResolutionState,
377
- scope: Scope,
378
- feature: ConfigFeature | "document",
379
- code: string,
380
- inputPath: string,
381
- ): void {
382
- state.issues.push(
383
- withModel(scope, { severity: "error", code, path: inputPath, feature }),
384
- );
385
- if (scopeApplies(state, scope)) {
386
- if (feature === "document") markModelOverrideInvalid(state, scope);
387
- else state.hardInvalid.add(feature);
388
- }
389
- }
390
-
391
- function markModelOverrideInvalid(state: ResolutionState, scope: Scope): void {
392
- if (!scopeApplies(state, scope)) return;
393
- for (const feature of MODEL_OVERRIDABLE_FEATURES) state.hardInvalid.add(feature);
394
- }
395
-
396
- /**
397
- * Unknown properties are errors in their owning scope. A key named after a feature (or its
398
- * legacy alias) fails that feature closed; otherwise the containing feature scope fails closed;
399
- * an unmapped key blocks its containing model scope (or the document for a root/default key).
400
- */
401
- function reportUnknownKeys(
402
- container: Record<string, unknown>,
403
- known: ReadonlySet<string>,
404
- scope: Scope,
405
- state: ResolutionState,
406
- owningFeature: ConfigFeature | undefined,
407
- ): void {
408
- for (const key of Object.keys(container)) {
409
- if (known.has(key)) continue;
410
- const feature = owningFeature ?? FEATURE_NAMES.get(key) ?? "document";
411
- state.issues.push(
412
- withModel(scope, {
413
- severity: "error",
414
- code: V2_ISSUE.keyUnknown,
415
- path: joinPath(scope.input, "[unknown-field]"),
416
- feature,
417
- }),
418
- );
419
- if (scopeApplies(state, scope)) {
420
- if (feature !== "document") state.hardInvalid.add(feature);
421
- else if (scope.kind === "model") markModelOverrideInvalid(state, scope);
422
- else state.untrusted = true;
423
- }
424
- }
425
- }
426
-
427
- // ---------------------------------------------------------------------------------------------
428
- // Value readers
429
- // ---------------------------------------------------------------------------------------------
430
-
431
- type ReadResult<T> = { ok: true; value: T } | { ok: false; code: string };
432
-
433
- function typeCode(value: unknown, expected: string): string {
434
- return value === null ? V2_ISSUE.nullUnsupported : expected;
435
- }
436
-
437
- function readBoolean(value: unknown): ReadResult<boolean> {
438
- return typeof value === "boolean"
439
- ? { ok: true, value }
440
- : { ok: false, code: typeCode(value, V2_ISSUE.expectedBoolean) };
441
- }
442
-
443
- function readEnum<T extends string>(allowed: readonly T[], value: unknown): ReadResult<T> {
444
- if (typeof value !== "string") {
445
- return { ok: false, code: typeCode(value, V2_ISSUE.expectedString) };
446
- }
447
- const normalized = value.trim();
448
- for (const entry of allowed) {
449
- if (entry === normalized) return { ok: true, value: entry };
450
- }
451
- return { ok: false, code: V2_ISSUE.expectedEnum };
452
- }
453
-
454
- function readInteger(value: unknown, min: number, max: number): ReadResult<number> {
455
- if (typeof value !== "number") {
456
- return { ok: false, code: typeCode(value, V2_ISSUE.expectedInteger) };
457
- }
458
- if (!Number.isInteger(value) || value < min || value > max) {
459
- return { ok: false, code: V2_ISSUE.range };
460
- }
461
- return { ok: true, value };
462
- }
463
-
464
- function readThinkingLevel(value: unknown): ReadResult<ThinkingLevel> {
465
- return readEnum(THINKING_LEVELS, value);
466
- }
467
-
468
- /** A non-empty, trimmed string such as `artifactRoot`. */
469
- function readNonEmptyString(value: unknown, maxChars?: number): ReadResult<string> {
470
- if (typeof value !== "string") {
471
- return { ok: false, code: typeCode(value, V2_ISSUE.expectedString) };
472
- }
473
- const normalized = value.trim();
474
- if (normalized.length === 0) return { ok: false, code: V2_ISSUE.empty };
475
- if (maxChars !== undefined && normalized.length > maxChars) {
476
- return { ok: false, code: V2_ISSUE.length };
477
- }
478
- return { ok: true, value: normalized };
479
- }
480
-
481
- /** Exact `provider/model-id` reference; `null` clears an inherited reference. */
482
- function readModelRef(value: unknown): ReadResult<string | null> {
483
- if (value === null) return { ok: true, value: null };
484
- if (typeof value !== "string") {
485
- return { ok: false, code: V2_ISSUE.expectedModel };
486
- }
487
- const normalized = value.trim();
488
- if (normalized.length === 0) return { ok: false, code: V2_ISSUE.empty };
489
- if (!isExactModelKey(normalized)) return { ok: false, code: V2_ISSUE.expectedModel };
490
- return { ok: true, value: normalized };
491
- }
492
-
493
- /** Order-preserving string list with the legacy trim/dedupe/drop-blank normalization. */
494
- function readStringList(
495
- value: unknown,
496
- options?: { maxEntryChars?: number; requireNonEmpty?: boolean },
497
- ): ReadResult<string[]> {
498
- if (!Array.isArray(value)) {
499
- return { ok: false, code: typeCode(value, V2_ISSUE.expectedList) };
500
- }
501
- const entries: string[] = [];
502
- for (const item of value) {
503
- if (typeof item !== "string") {
504
- return { ok: false, code: V2_ISSUE.expectedString };
505
- }
506
- const normalized = item.trim();
507
- if (normalized.length === 0) continue;
508
- if (options?.maxEntryChars !== undefined && normalized.length > options.maxEntryChars) {
509
- return { ok: false, code: V2_ISSUE.length };
510
- }
511
- if (!entries.includes(normalized)) entries.push(normalized);
512
- }
513
- if (options?.requireNonEmpty && entries.length === 0) {
514
- return { ok: false, code: V2_ISSUE.empty };
515
- }
516
- return { ok: true, value: entries };
517
- }
518
-
519
- function readImageModelList(value: unknown): ReadResult<string[]> {
520
- return readStringList(value, {
521
- maxEntryChars: MAX_IMAGE_MODEL_ID_CHARS,
522
- requireNonEmpty: true,
523
- });
524
- }
525
-
526
- // ---------------------------------------------------------------------------------------------
527
- // Field application
528
- // ---------------------------------------------------------------------------------------------
529
-
530
- /**
531
- * Apply one known leaf when it is present: validate, and on success write the effective value,
532
- * record its origin and mark the leaf supplied for the resolved model. An invalid present value
533
- * is reported and leaves the previously resolved value and origin untouched.
534
- */
535
- function applyField<T>(
536
- container: Record<string, unknown>,
537
- field: FieldDescriptor,
538
- scope: Scope,
539
- state: ResolutionState,
540
- read: (value: unknown) => ReadResult<T>,
541
- assign: (value: T) => void,
542
- ): void {
543
- if (!Object.hasOwn(container, field.key)) return;
544
- const inputPath = joinPath(scope.input, field.key);
545
- const result = read(container[field.key]);
546
- if (!result.ok) {
547
- reportLeafError(state, scope, field, result.code, inputPath);
548
- return;
549
- }
550
- if (!scopeApplies(state, scope)) return;
551
- assign(result.value);
552
- state.origins[field.leaf] = {
553
- kind: scope.kind === "defaults" ? "defaults" : "model",
554
- path: inputPath,
555
- };
556
- if (scope.kind === "model") leafState(state, field.leaf, field.feature).modelApplied = true;
557
- }
558
-
559
- /** Open a nested known object; a present non-object is a scope error for the owning feature. */
560
- function openGroup(
561
- container: Record<string, unknown>,
562
- key: string,
563
- scope: Scope,
564
- state: ResolutionState,
565
- feature: ConfigFeature,
566
- ): { raw: Record<string, unknown>; scope: Scope } | undefined {
567
- if (!Object.hasOwn(container, key)) return undefined;
568
- const value = container[key];
569
- if (!isRecord(value)) {
570
- reportScopeError(state, scope, feature, V2_ISSUE.scopeInvalid, joinPath(scope.input, key));
571
- return undefined;
572
- }
573
- return { raw: value, scope: subScope(scope, key) };
574
- }
575
-
576
- // ---------------------------------------------------------------------------------------------
577
- // Feature scopes
578
- // ---------------------------------------------------------------------------------------------
579
-
580
- function applyContextSettings(
581
- raw: Record<string, unknown>,
582
- scope: Scope,
583
- state: ResolutionState,
584
- ): void {
585
- reportUnknownKeys(raw, CONTEXT_KEYS, scope, state, "context");
586
-
587
- applyField(
588
- raw,
589
- { key: "mode", leaf: "context.mode", feature: "context" },
590
- scope,
591
- state,
592
- (value) => readEnum(CONTEXT_MODES, value),
593
- (value) => {
594
- state.policy.context.mode = value;
595
- },
596
- );
597
-
598
- const remoteCompaction = openGroup(raw, "remoteCompaction", scope, state, "context");
599
- if (remoteCompaction) applyRemoteCompactionSettings(remoteCompaction.raw, remoteCompaction.scope, state);
600
-
601
- const nativeFallback = openGroup(raw, "nativeFallback", scope, state, "context");
602
- if (nativeFallback) applyNativeFallbackSettings(nativeFallback.raw, nativeFallback.scope, state);
603
-
604
- const remoteWindows = openGroup(raw, "remoteWindows", scope, state, "context");
605
- if (remoteWindows) applyRemoteWindowsSettings(remoteWindows.raw, remoteWindows.scope, state);
606
- }
607
-
608
- function applyRemoteCompactionSettings(
609
- raw: Record<string, unknown>,
610
- scope: Scope,
611
- state: ResolutionState,
612
- ): void {
613
- reportUnknownKeys(raw, REMOTE_COMPACTION_KEYS, scope, state, "context");
614
-
615
- applyField(
616
- raw,
617
- { key: "model", leaf: "context.remoteCompaction.model", feature: "context" },
618
- scope,
619
- state,
620
- readModelRef,
621
- (value) => {
622
- state.policy.context.remoteCompaction.model = value;
623
- },
624
- );
625
- applyField(
626
- raw,
627
- { key: "inputSource", leaf: "context.remoteCompaction.inputSource", feature: "context" },
628
- scope,
629
- state,
630
- (value) => readEnum(INPUT_SOURCES, value),
631
- (value) => {
632
- state.policy.context.remoteCompaction.inputSource = value;
633
- },
634
- );
635
- applyField(
636
- raw,
637
- {
638
- key: "allowContinuityBreak",
639
- leaf: "context.remoteCompaction.allowContinuityBreak",
640
- feature: "context",
641
- },
642
- scope,
643
- state,
644
- readBoolean,
645
- (value) => {
646
- state.policy.context.remoteCompaction.allowContinuityBreak = value;
647
- },
648
- );
649
- applyApisField(raw, scope, state);
650
- }
651
-
652
- /**
653
- * `apis` may only narrow the Responses APIs the toolkit can build a compact URL for, like the
654
- * legacy decoder. Unsupported entries invalidate this leaf; a malformed selected policy
655
- * cannot silently fall back to the inherited API list.
656
- */
657
- function applyApisField(raw: Record<string, unknown>, scope: Scope, state: ResolutionState): void {
658
- if (!Object.hasOwn(raw, "apis")) return;
659
- const field: FieldDescriptor = {
660
- key: "apis",
661
- leaf: "context.remoteCompaction.apis",
662
- feature: "context",
663
- };
664
- const inputPath = joinPath(scope.input, "apis");
665
- const value = raw.apis;
666
- if (!Array.isArray(value)) {
667
- reportLeafError(state, scope, field, typeCode(value, V2_ISSUE.expectedList), inputPath);
668
- return;
669
- }
670
-
671
- const accepted: string[] = [];
672
- for (let index = 0; index < value.length; index += 1) {
673
- const entry = value[index];
674
- if (typeof entry !== "string") {
675
- reportLeafError(state, scope, field, V2_ISSUE.expectedString, inputPath);
676
- return;
677
- }
678
- const normalized = entry.trim();
679
- if (normalized.length === 0) continue;
680
- const capable = RESPONSES_COMPACT_CAPABLE_APIS as readonly string[];
681
- if (!capable.includes(normalized)) {
682
- reportLeafError(state, scope, field, V2_ISSUE.apiUnsupported, `${inputPath}[${index}]`);
683
- return;
684
- }
685
- if (!accepted.includes(normalized)) accepted.push(normalized);
686
- }
687
-
688
- if (!scopeApplies(state, scope)) return;
689
- state.policy.context.remoteCompaction.apis = accepted;
690
- state.origins[field.leaf] = {
691
- kind: scope.kind === "defaults" ? "defaults" : "model",
692
- path: inputPath,
693
- };
694
- if (scope.kind === "model") leafState(state, field.leaf, field.feature).modelApplied = true;
695
- }
696
-
697
- function applyNativeFallbackSettings(
698
- raw: Record<string, unknown>,
699
- scope: Scope,
700
- state: ResolutionState,
701
- ): void {
702
- reportUnknownKeys(raw, NATIVE_FALLBACK_KEYS, scope, state, "context");
703
-
704
- applyField(
705
- raw,
706
- { key: "enabled", leaf: "context.nativeFallback.enabled", feature: "context" },
707
- scope,
708
- state,
709
- readBoolean,
710
- (value) => {
711
- state.policy.context.nativeFallback.enabled = value;
712
- },
713
- );
714
- applyField(
715
- raw,
716
- { key: "model", leaf: "context.nativeFallback.model", feature: "context" },
717
- scope,
718
- state,
719
- readModelRef,
720
- (value) => {
721
- state.policy.context.nativeFallback.model = value;
722
- },
723
- );
724
- applyField(
725
- raw,
726
- { key: "thinkingLevel", leaf: "context.nativeFallback.thinkingLevel", feature: "context" },
727
- scope,
728
- state,
729
- readThinkingLevel,
730
- (value) => {
731
- state.policy.context.nativeFallback.thinkingLevel = value;
732
- },
733
- );
734
- }
735
-
736
- function applyRemoteWindowsSettings(
737
- raw: Record<string, unknown>,
738
- scope: Scope,
739
- state: ResolutionState,
740
- ): void {
741
- reportUnknownKeys(raw, REMOTE_WINDOWS_KEYS, scope, state, "context");
742
-
743
- applyField(
744
- raw,
745
- { key: "leaveManagedMode", leaf: "context.remoteWindows.leaveManagedMode", feature: "context" },
746
- scope,
747
- state,
748
- (value) => readEnum(LEAVE_MANAGED_MODES, value),
749
- (value) => {
750
- state.policy.context.remoteWindows.leaveManagedMode = value;
751
- },
752
- );
753
- applyField(
754
- raw,
755
- {
756
- key: "reminderThresholdPercent",
757
- leaf: "context.remoteWindows.reminderThresholdPercent",
758
- feature: "context",
759
- },
760
- scope,
761
- state,
762
- (value) => readInteger(value, 0, 100),
763
- (value) => {
764
- state.policy.context.remoteWindows.reminderThresholdPercent = value;
765
- },
766
- );
767
- }
768
-
769
- function applyWebSearchSettings(
770
- raw: Record<string, unknown>,
771
- scope: Scope,
772
- state: ResolutionState,
773
- ): void {
774
- reportUnknownKeys(raw, WEB_SEARCH_KEYS, scope, state, "webSearch");
775
-
776
- applyField(
777
- raw,
778
- { key: "route", leaf: "webSearch.route", feature: "webSearch" },
779
- scope,
780
- state,
781
- (value) => readEnum(SEARCH_ROUTES, value),
782
- (value) => {
783
- state.policy.webSearch.route = value satisfies SearchRoute;
784
- },
785
- );
786
- }
787
-
788
- /**
789
- * Image output settings are global only. The effective default must be a member of the effective
790
- * non-empty allowed list; a mismatch or an empty list fails the paid feature closed instead of
791
- * selecting a different model.
792
- */
793
- function applyImageGenerationSettings(
794
- raw: Record<string, unknown>,
795
- scope: Scope,
796
- state: ResolutionState,
797
- ): void {
798
- reportUnknownKeys(raw, IMAGE_GENERATION_KEYS, scope, state, "imageGeneration");
799
-
800
- applyField(
801
- raw,
802
- { key: "enabled", leaf: "imageGeneration.enabled", feature: "imageGeneration" },
803
- scope,
804
- state,
805
- readBoolean,
806
- (value) => {
807
- state.policy.imageGeneration.enabled = value;
808
- },
809
- );
810
- applyField(
811
- raw,
812
- { key: "defaultModel", leaf: "imageGeneration.defaultModel", feature: "imageGeneration" },
813
- scope,
814
- state,
815
- (value) => readNonEmptyString(value, MAX_IMAGE_MODEL_ID_CHARS),
816
- (value) => {
817
- state.policy.imageGeneration.defaultModel = value;
818
- },
819
- );
820
- applyField(
821
- raw,
822
- { key: "allowedModels", leaf: "imageGeneration.allowedModels", feature: "imageGeneration" },
823
- scope,
824
- state,
825
- readImageModelList,
826
- (value) => {
827
- state.policy.imageGeneration.allowedModels = value;
828
- },
829
- );
830
-
831
- const defaultLeaf = state.leaves.get("imageGeneration.defaultModel");
832
- const allowedLeaf = state.leaves.get("imageGeneration.allowedModels");
833
- const unreliable = (leaf: LeafState | undefined): boolean =>
834
- !!leaf && (leaf.invalidAtDefaults || leaf.invalidAtModel);
835
- if (unreliable(defaultLeaf) || unreliable(allowedLeaf)) return;
836
-
837
- const image = state.policy.imageGeneration;
838
- if (image.allowedModels.includes(image.defaultModel)) return;
839
- reportScopeError(
840
- state,
841
- scope,
842
- "imageGeneration",
843
- V2_ISSUE.imageDefaultNotAllowed,
844
- joinPath(scope.input, "defaultModel"),
845
- );
846
- }
847
-
848
- function applyResponsesWebSocketSettings(
849
- raw: Record<string, unknown>,
850
- scope: Scope,
851
- state: ResolutionState,
852
- ): void {
853
- reportUnknownKeys(raw, RESPONSES_WEBSOCKET_KEYS, scope, state, "responsesWebSocket");
854
-
855
- applyField(
856
- raw,
857
- { key: "enabled", leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
858
- scope,
859
- state,
860
- readBoolean,
861
- (value) => {
862
- state.policy.responsesWebSocket.enabled = value;
863
- },
864
- );
865
- }
866
-
867
- function applyAutoModeSettings(
868
- raw: Record<string, unknown>,
869
- scope: Scope,
870
- state: ResolutionState,
871
- ): void {
872
- reportUnknownKeys(raw, AUTO_MODE_KEYS, scope, state, "autoMode");
873
-
874
- applyField(
875
- raw,
876
- { key: "available", leaf: "autoMode.available", feature: "autoMode" },
877
- scope,
878
- state,
879
- readBoolean,
880
- (value) => {
881
- state.policy.autoMode.available = value;
882
- },
883
- );
884
- applyField(
885
- raw,
886
- { key: "reviewerModel", leaf: "autoMode.reviewerModel", feature: "autoMode" },
887
- scope,
888
- state,
889
- readModelRef,
890
- (value) => {
891
- state.policy.autoMode.reviewerModel = value;
892
- },
893
- );
894
- applyField(
895
- raw,
896
- { key: "gate", leaf: "autoMode.gate", feature: "autoMode" },
897
- scope,
898
- state,
899
- (value) => readEnum(AUTO_GATES, value),
900
- (value) => {
901
- state.policy.autoMode.gate = value;
902
- },
903
- );
904
- applyField(
905
- raw,
906
- { key: "extraTools", leaf: "autoMode.extraTools", feature: "autoMode" },
907
- scope,
908
- state,
909
- readStringList,
910
- (value) => {
911
- state.policy.autoMode.extraTools = value;
912
- },
913
- );
914
- applyField(
915
- raw,
916
- { key: "timeoutMs", leaf: "autoMode.timeoutMs", feature: "autoMode" },
917
- scope,
918
- state,
919
- (value) => readInteger(value, REVIEWER_TIMEOUT_MIN_MS, REVIEWER_TIMEOUT_MAX_MS),
920
- (value) => {
921
- state.policy.autoMode.timeoutMs = value;
922
- },
923
- );
924
- applyField(
925
- raw,
926
- { key: "transcript", leaf: "autoMode.transcript", feature: "autoMode" },
927
- scope,
928
- state,
929
- readBoolean,
930
- (value) => {
931
- state.policy.autoMode.transcript = value;
932
- },
933
- );
934
- applyField(
935
- raw,
936
- { key: "evidenceTools", leaf: "autoMode.evidenceTools", feature: "autoMode" },
937
- scope,
938
- state,
939
- readBoolean,
940
- (value) => {
941
- state.policy.autoMode.evidenceTools = value;
942
- },
943
- );
944
- applyField(
945
- raw,
946
- { key: "maxEvidenceRounds", leaf: "autoMode.maxEvidenceRounds", feature: "autoMode" },
947
- scope,
948
- state,
949
- (value) => readInteger(value, EVIDENCE_ROUNDS_MIN, EVIDENCE_ROUNDS_MAX),
950
- (value) => {
951
- state.policy.autoMode.maxEvidenceRounds = value;
952
- },
953
- );
954
-
955
- const classifier = openGroup(raw, "classifier", scope, state, "autoMode");
956
- if (classifier) applyClassifierSettings(classifier.raw, classifier.scope, state);
957
-
958
- const circuitBreaker = openGroup(raw, "circuitBreaker", scope, state, "autoMode");
959
- if (circuitBreaker) applyCircuitBreakerSettings(circuitBreaker.raw, circuitBreaker.scope, state);
960
- }
961
-
962
- function applyClassifierSettings(
963
- raw: Record<string, unknown>,
964
- scope: Scope,
965
- state: ResolutionState,
966
- ): void {
967
- reportUnknownKeys(raw, CLASSIFIER_KEYS, scope, state, "autoMode");
968
-
969
- applyField(
970
- raw,
971
- { key: "enabled", leaf: "autoMode.classifier.enabled", feature: "autoMode" },
972
- scope,
973
- state,
974
- readBoolean,
975
- (value) => {
976
- state.policy.autoMode.classifier.enabled = value;
977
- },
978
- );
979
- applyField(
980
- raw,
981
- { key: "model", leaf: "autoMode.classifier.model", feature: "autoMode" },
982
- scope,
983
- state,
984
- readModelRef,
985
- (value) => {
986
- state.policy.autoMode.classifier.model = value;
987
- },
988
- );
989
- applyField(
990
- raw,
991
- { key: "timeoutMs", leaf: "autoMode.classifier.timeoutMs", feature: "autoMode" },
992
- scope,
993
- state,
994
- (value) => readInteger(value, REVIEWER_TIMEOUT_MIN_MS, REVIEWER_TIMEOUT_MAX_MS),
995
- (value) => {
996
- state.policy.autoMode.classifier.timeoutMs = value;
997
- },
998
- );
999
- applyField(
1000
- raw,
1001
- { key: "maxLag", leaf: "autoMode.classifier.maxLag", feature: "autoMode" },
1002
- scope,
1003
- state,
1004
- (value) => readInteger(value, CLASSIFIER_MAX_LAG_MIN, CLASSIFIER_MAX_LAG_MAX),
1005
- (value) => {
1006
- state.policy.autoMode.classifier.maxLag = value;
1007
- },
1008
- );
1009
- }
1010
-
1011
- function applyCircuitBreakerSettings(
1012
- raw: Record<string, unknown>,
1013
- scope: Scope,
1014
- state: ResolutionState,
1015
- ): void {
1016
- reportUnknownKeys(raw, BREAKER_KEYS, scope, state, "autoMode");
1017
-
1018
- applyField(
1019
- raw,
1020
- {
1021
- key: "consecutiveDenials",
1022
- leaf: "autoMode.circuitBreaker.consecutiveDenials",
1023
- feature: "autoMode",
1024
- },
1025
- scope,
1026
- state,
1027
- (value) => readInteger(value, BREAKER_LIMIT_MIN, BREAKER_LIMIT_MAX),
1028
- (value) => {
1029
- state.policy.autoMode.circuitBreaker.consecutiveDenials = value;
1030
- },
1031
- );
1032
- applyField(
1033
- raw,
1034
- { key: "recentDenials", leaf: "autoMode.circuitBreaker.recentDenials", feature: "autoMode" },
1035
- scope,
1036
- state,
1037
- (value) => readInteger(value, BREAKER_LIMIT_MIN, BREAKER_LIMIT_MAX),
1038
- (value) => {
1039
- state.policy.autoMode.circuitBreaker.recentDenials = value;
1040
- },
1041
- );
1042
- applyField(
1043
- raw,
1044
- { key: "windowSize", leaf: "autoMode.circuitBreaker.windowSize", feature: "autoMode" },
1045
- scope,
1046
- state,
1047
- (value) => readInteger(value, BREAKER_WINDOW_MIN, BREAKER_WINDOW_MAX),
1048
- (value) => {
1049
- state.policy.autoMode.circuitBreaker.windowSize = value;
1050
- },
1051
- );
1052
- }
1053
-
1054
- /** Protocol compatibility is an exact-model opt-in; there is no broad defaults wildcard. */
1055
- function applyCompatibilitySettings(
1056
- raw: Record<string, unknown>,
1057
- scope: Scope,
1058
- state: ResolutionState,
1059
- ): void {
1060
- reportUnknownKeys(raw, COMPATIBILITY_KEYS, scope, state, "compatibility");
1061
- if (scope.modelKey && Object.hasOwn(raw, "transport") && typeof raw.transport === "string" && raw.transport.trim() === "codex-gateway") {
1062
- state.gatewayCandidates.add(scope.modelKey);
1063
- }
1064
-
1065
- applyField(
1066
- raw,
1067
- { key: "transport", leaf: "compatibility.transport", feature: "compatibility" },
1068
- scope,
1069
- state,
1070
- (value) => readEnum(TRANSPORTS, value),
1071
- (value) => {
1072
- state.policy.compatibility.transport = value;
1073
- },
1074
- );
1075
- }
1076
-
1077
- function applyDiagnosticsSettings(
1078
- raw: Record<string, unknown>,
1079
- scope: Scope,
1080
- state: ResolutionState,
1081
- ): void {
1082
- reportUnknownKeys(raw, DIAGNOSTICS_KEYS, scope, state, "diagnostics");
1083
-
1084
- applyField(
1085
- raw,
1086
- { key: "level", leaf: "diagnostics.level", feature: "diagnostics" },
1087
- scope,
1088
- state,
1089
- (value) => readEnum(DIAGNOSTIC_LEVELS, value),
1090
- (value) => {
1091
- state.policy.diagnostics.level = value;
1092
- },
1093
- );
1094
- applyField(
1095
- raw,
1096
- { key: "notifyOnLoad", leaf: "diagnostics.notifyOnLoad", feature: "diagnostics" },
1097
- scope,
1098
- state,
1099
- readBoolean,
1100
- (value) => {
1101
- state.policy.diagnostics.notifyOnLoad = value;
1102
- },
1103
- );
1104
- applyField(
1105
- raw,
1106
- { key: "captureRequests", leaf: "diagnostics.captureRequests", feature: "diagnostics" },
1107
- scope,
1108
- state,
1109
- readBoolean,
1110
- (value) => {
1111
- state.policy.diagnostics.captureRequests = value;
1112
- },
1113
- );
1114
- applyField(
1115
- raw,
1116
- { key: "captureResponses", leaf: "diagnostics.captureResponses", feature: "diagnostics" },
1117
- scope,
1118
- state,
1119
- readBoolean,
1120
- (value) => {
1121
- state.policy.diagnostics.captureResponses = value;
1122
- },
1123
- );
1124
- applyField(
1125
- raw,
1126
- {
1127
- key: "redactSensitiveData",
1128
- leaf: "diagnostics.redactSensitiveData",
1129
- feature: "diagnostics",
1130
- },
1131
- scope,
1132
- state,
1133
- readBoolean,
1134
- (value) => {
1135
- state.policy.diagnostics.redactSensitiveData = value;
1136
- },
1137
- );
1138
- applyField(
1139
- raw,
1140
- { key: "artifactRoot", leaf: "diagnostics.artifactRoot", feature: "diagnostics" },
1141
- scope,
1142
- state,
1143
- readNonEmptyString,
1144
- (value) => {
1145
- // Path expansion stays in the loader; the resolver records the configured value.
1146
- state.policy.diagnostics.artifactRoot = value;
1147
- },
1148
- );
1149
- }
1150
-
1151
- /** Reusable scope appliers so `defaults.<feature>` and `models[key].<feature>` share one contract. */
1152
- function applyContextScope(container: Record<string, unknown>, scope: Scope, state: ResolutionState): void {
1153
- const group = openGroup(container, "context", scope, state, "context");
1154
- if (group) applyContextSettings(group.raw, group.scope, state);
1155
- }
1156
-
1157
- function applyWebSearchScope(
1158
- container: Record<string, unknown>,
1159
- scope: Scope,
1160
- state: ResolutionState,
1161
- ): void {
1162
- const group = openGroup(container, "webSearch", scope, state, "webSearch");
1163
- if (group) applyWebSearchSettings(group.raw, group.scope, state);
1164
- }
1165
-
1166
- function applyImageGenerationScope(
1167
- container: Record<string, unknown>,
1168
- scope: Scope,
1169
- state: ResolutionState,
1170
- ): void {
1171
- const group = openGroup(container, "imageGeneration", scope, state, "imageGeneration");
1172
- if (group) applyImageGenerationSettings(group.raw, group.scope, state);
1173
- }
1174
-
1175
- function applyResponsesWebSocketScope(
1176
- container: Record<string, unknown>,
1177
- scope: Scope,
1178
- state: ResolutionState,
1179
- ): void {
1180
- const group = openGroup(container, "responsesWebSocket", scope, state, "responsesWebSocket");
1181
- if (group) applyResponsesWebSocketSettings(group.raw, group.scope, state);
1182
- }
1183
-
1184
- function applyAutoModeScope(
1185
- container: Record<string, unknown>,
1186
- scope: Scope,
1187
- state: ResolutionState,
1188
- ): void {
1189
- const group = openGroup(container, "autoMode", scope, state, "autoMode");
1190
- if (group) applyAutoModeSettings(group.raw, group.scope, state);
1191
- }
1192
-
1193
- function applyCompatibilityScope(
1194
- container: Record<string, unknown>,
1195
- scope: Scope,
1196
- state: ResolutionState,
1197
- ): void {
1198
- const group = openGroup(container, "compatibility", scope, state, "compatibility");
1199
- if (group) applyCompatibilitySettings(group.raw, group.scope, state);
1200
- }
1201
-
1202
- // ---------------------------------------------------------------------------------------------
1203
- // Document sections
1204
- // ---------------------------------------------------------------------------------------------
1205
-
1206
- function applyDefaults(doc: Record<string, unknown>, state: ResolutionState): void {
1207
- if (!Object.hasOwn(doc, "defaults")) return;
1208
- const raw = doc.defaults;
1209
- if (!isRecord(raw)) {
1210
- state.issues.push({
1211
- severity: "error",
1212
- code: V2_ISSUE.defaultsInvalid,
1213
- path: "defaults",
1214
- feature: "document",
1215
- });
1216
- state.untrusted = true;
1217
- return;
1218
- }
1219
-
1220
- const scope: Scope = { input: "defaults", kind: "defaults" };
1221
- reportUnknownKeys(raw, DEFAULTS_KEYS, scope, state, undefined);
1222
- applyContextScope(raw, scope, state);
1223
- applyWebSearchScope(raw, scope, state);
1224
- applyImageGenerationScope(raw, scope, state);
1225
- applyResponsesWebSocketScope(raw, scope, state);
1226
- applyAutoModeScope(raw, scope, state);
1227
- }
1228
-
1229
- function applyModels(doc: Record<string, unknown>, state: ResolutionState): void {
1230
- if (!Object.hasOwn(doc, "models")) return;
1231
- const raw = doc.models;
1232
- if (!isRecord(raw)) {
1233
- state.issues.push({
1234
- severity: "error",
1235
- code: V2_ISSUE.modelsInvalid,
1236
- path: "models",
1237
- feature: "document",
1238
- });
1239
- markModelOverrideInvalid(state, { input: "models", kind: "document" });
1240
- return;
1241
- }
1242
-
1243
- // Trim-normalize keys and reject collisions instead of resolving them last-wins.
1244
- // Keep the raw key for JSON paths: normalization selects the scope, but provenance and
1245
- // diagnostics must still point at the property the caller actually supplied.
1246
- const normalized = new Map<string, string[]>();
1247
- for (const rawKey of Object.keys(raw)) {
1248
- const key = rawKey.trim();
1249
- const existing = normalized.get(key);
1250
- if (existing) existing.push(rawKey);
1251
- else normalized.set(key, [rawKey]);
1252
- }
1253
-
1254
- for (const [key, rawKeys] of normalized) {
1255
- const firstRawKey = rawKeys[0] as string;
1256
- const scope: Scope = { input: modelScopePath(firstRawKey), kind: "model", modelKey: key };
1257
- if (!isExactModelKey(key)) {
1258
- state.issues.push({
1259
- severity: "error",
1260
- code: V2_ISSUE.keyInvalid,
1261
- path: modelScopePath(boundedSegment(firstRawKey)),
1262
- feature: "document",
1263
- });
1264
- if (key === state.selectedKey) markModelOverrideInvalid(state, scope);
1265
- continue;
1266
- }
1267
- if (rawKeys.length > 1) {
1268
- state.issues.push(
1269
- withModel(scope, {
1270
- severity: "error",
1271
- code: V2_ISSUE.keyDuplicate,
1272
- path: modelScopePath(boundedSegment(firstRawKey)),
1273
- feature: "document",
1274
- }),
1275
- );
1276
- if (key === state.selectedKey) markModelOverrideInvalid(state, scope);
1277
- continue;
1278
- }
1279
-
1280
- const entry = Object.hasOwn(raw, firstRawKey) ? raw[firstRawKey] : undefined;
1281
- if (!isRecord(entry)) {
1282
- reportScopeError(state, scope, "document", V2_ISSUE.scopeInvalid, modelScopePath(boundedSegment(firstRawKey)));
1283
- markModelOverrideInvalid(state, scope);
1284
- continue;
1285
- }
1286
-
1287
- reportUnknownKeys(entry, MODEL_OVERRIDE_KEYS, scope, state, undefined);
1288
- applyContextScope(entry, scope, state);
1289
- applyWebSearchScope(entry, scope, state);
1290
- applyAutoModeScope(entry, scope, state);
1291
- applyCompatibilityScope(entry, scope, state);
1292
- }
1293
- }
1294
-
1295
- function applyDiagnostics(doc: Record<string, unknown>, state: ResolutionState): void {
1296
- if (!Object.hasOwn(doc, "diagnostics")) return;
1297
- const raw = doc.diagnostics;
1298
- if (!isRecord(raw)) {
1299
- reportScopeError(
1300
- state,
1301
- { input: "diagnostics", kind: "document" },
1302
- "diagnostics",
1303
- V2_ISSUE.diagnosticsInvalid,
1304
- "diagnostics",
1305
- );
1306
- return;
1307
- }
1308
- // Diagnostics are global, so they resolve into the inherited policy like `defaults`.
1309
- applyDiagnosticsSettings(raw, { input: "diagnostics", kind: "defaults" }, state);
1310
- }
1311
-
1312
- type V2Resolution = ResolvedToolkitPolicy & { gatewayModelKeys: string[] };
1313
-
1314
- function finish(state: ResolutionState): V2Resolution {
1315
- const invalidFeatures = CONFIG_FEATURES.filter((feature) => {
1316
- if (state.untrusted || state.hardInvalid.has(feature)) return true;
1317
- for (const leaf of state.leaves.values()) {
1318
- if (leaf.feature !== feature) continue;
1319
- if (leaf.invalidAtModel) return true;
1320
- if (leaf.invalidAtDefaults && !leaf.modelApplied) return true;
1321
- }
1322
- return false;
1323
- });
1324
- // All model scopes were validated in the same pass. An unrelated context/search error
1325
- // does not change a valid transport; document/compatibility errors do.
1326
- const blockedGateways = new Set<string>();
1327
- let blockAllGateways = state.untrusted;
1328
- for (const issue of state.issues) {
1329
- if (issue.severity !== "error" || (issue.feature !== "document" && issue.feature !== "compatibility")) continue;
1330
- if (issue.modelKey !== undefined) blockedGateways.add(issue.modelKey);
1331
- else if (issue.code !== V2_ISSUE.keyInvalid) blockAllGateways = true;
1332
- }
1333
- return {
1334
- policy: state.policy,
1335
- origins: state.origins,
1336
- issues: state.issues,
1337
- invalidFeatures: [...invalidFeatures],
1338
- gatewayModelKeys: blockAllGateways ? [] : [...state.gatewayCandidates].filter((key) => !blockedGateways.has(key)),
1339
- };
1340
- }
1341
-
1342
- /**
1343
- * Validate and resolve a version 2 document for `modelKey`.
1344
- *
1345
- * Pure: no file I/O, path expansion, auth lookup, provider call or tool mutation. Callers must
1346
- * route unversioned documents through the legacy adapter instead. Consumers must treat any
1347
- * feature listed in `invalidFeatures` as fail-closed and must never reconstruct the merge
1348
- * themselves.
1349
- */
1350
- export function resolveV2Config(raw: unknown, modelKey: string | undefined): V2Resolution {
1351
- const state = createState(modelKey);
1352
-
1353
- if (!isRecord(raw)) {
1354
- fatalDocumentError(state, V2_ISSUE.rootInvalid, "document");
1355
- return finish(state);
1356
- }
1357
- if (!Object.hasOwn(raw, "schemaVersion")) {
1358
- fatalDocumentError(state, V2_ISSUE.schemaVersionMissing, "schemaVersion");
1359
- return finish(state);
1360
- }
1361
- if (raw.schemaVersion !== 2) {
1362
- fatalDocumentError(state, V2_ISSUE.schemaVersionUnsupported, "schemaVersion");
1363
- return finish(state);
1364
- }
1365
- if (LEGACY_ROOT_KEYS.some((key) => Object.hasOwn(raw, key))) {
1366
- fatalDocumentError(state, V2_ISSUE.formatMixed, "document");
1367
- return finish(state);
1368
- }
1369
-
1370
- if (Object.hasOwn(raw, "$schema") && typeof raw.$schema !== "string") {
1371
- fatalDocumentError(state, V2_ISSUE.schemaInvalid, "$schema");
1372
- }
1373
- reportUnknownKeys(raw, ROOT_KEYS, DOCUMENT_SCOPE, state, undefined);
1374
-
1375
- applyDefaults(raw, state);
1376
- applyModels(raw, state);
1377
- applyDiagnostics(raw, state);
1378
-
1379
- return finish(state);
1380
- }
1
+ /**
2
+ * Version 2 global Toolkit configuration: validation, known-field merge and per-leaf
3
+ * provenance.
4
+ *
5
+ * This module is a pure decoder. It performs no file I/O, no path resolution, no auth or
6
+ * provider lookup and no tool mutation, and it deliberately depends on no JSON Schema
7
+ * library. The shipped `config.schema.json` mirrors the accepted document, and the runtime
8
+ * checks in this file stay the authority.
9
+ *
10
+ * Document shape (`schemaVersion` must be exactly 2):
11
+ *
12
+ * ```jsonc
13
+ * {
14
+ * "schemaVersion": 2,
15
+ * "$schema": "…metadata only…",
16
+ * "defaults": { "context": …, "webSearch": …, "imageGeneration": …, "responsesWebSocket": … },
17
+ * "models": { "provider/model-id": { "context": …, "webSearch": …, "compatibility": … } },
18
+ * "diagnostics": { … }
19
+ * }
20
+ * ```
21
+ *
22
+ * Rules:
23
+ * - Precedence is built-ins -> `defaults` -> the exact override of the resolved model.
24
+ * - Only known nested objects merge by field; arrays replace; `false` and `0` stay meaningful.
25
+ * - Active settings accept `null` only for optional producer/native-summary model references;
26
+ * it clears an inherited reference, never an object. Retired `autoMode` sections at defaults
27
+ * and model scope are ignored in any shape with a warning and never create effective policy.
28
+ * - Unknown properties are errors with masked names. Model-key diagnostic segments are bounded;
29
+ * raw values and unknown subtrees are never copied into diagnostics.
30
+ * - `defaults.compatibility` is not a recognized scope: the standard transport is internal and
31
+ * the Codex gateway transport stays an exact-model opt-in.
32
+ * - Image output settings (`defaults.imageGeneration`), Responses WebSocket transport
33
+ * (`defaults.responsesWebSocket`), and `diagnostics` are global only.
34
+ * - Exact model keys and model references use the legacy `provider/model-id` grammar: no
35
+ * whitespace, globs or bracket patterns. Keys are trimmed like the legacy decoder, and two
36
+ * raw keys that normalize to the same key are rejected instead of resolved last-wins.
37
+ *
38
+ * Fail-closed contract for consumers: `policy` always carries the best known value for every
39
+ * leaf, so an invalid present value keeps the inherited or built-in value instead of silently
40
+ * falling through to another route. The blocking signal is `invalidFeatures`, which is scoped to
41
+ * the resolved model: a leaf error in `defaults` marks the owning feature for every model, a
42
+ * leaf error in the resolved model's override marks that feature for that model only, an error
43
+ * in a different model is reported without invalidating the current selection, and a valid exact
44
+ * override shadows an invalid `defaults` leaf. Malformed document sections and unknown
45
+ * properties inside a feature scope are feature-level errors that no override can mask.
46
+ * `origins` explains the effective value of every leaf, so an ignored invalid value never claims
47
+ * provenance it does not own.
48
+ *
49
+ * Unversioned documents belong to the legacy adapter and must not be routed here: a missing or
50
+ * non-2 `schemaVersion`, a mixed legacy/v2 root and a non-object root are fatal document errors
51
+ * that resolve the shipped defaults and mark every feature invalid.
52
+ */
53
+
54
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
55
+ import { MAX_IMAGE_MODEL_ID_CHARS } from "../image-generation/types";
56
+ import {
57
+ RESPONSES_COMPACT_CAPABLE_APIS,
58
+ THINKING_LEVELS,
59
+ } from "../types";
60
+ import {
61
+ CONFIG_FEATURES,
62
+ RETIRED_AUTO_MODE_ISSUE,
63
+ createPolicyDefaults,
64
+ type ConfigFeature,
65
+ type ConfigIssue,
66
+ type ConfigOrigin,
67
+ type EffectiveToolkitPolicy,
68
+ type ResolvedToolkitPolicy,
69
+ type SearchRoute,
70
+ } from "./policy";
71
+
72
+ /**
73
+ * Stable, controlled diagnostic codes. Adding a code is a compatible change; consumers may
74
+ * match on these strings but must not build messages from raw configuration values.
75
+ */
76
+ export const V2_ISSUE = {
77
+ rootInvalid: "document.root.invalid",
78
+ schemaVersionMissing: "document.schemaVersion.missing",
79
+ schemaVersionUnsupported: "document.schemaVersion.unsupported",
80
+ formatMixed: "document.format.mixed",
81
+ schemaInvalid: "document.$schema.invalid",
82
+ defaultsInvalid: "document.defaults.invalid",
83
+ modelsInvalid: "document.models.invalid",
84
+ diagnosticsInvalid: "document.diagnostics.invalid",
85
+ scopeInvalid: "scope.invalid",
86
+ keyUnknown: "key.unknown",
87
+ keyInvalid: "models.key.invalid",
88
+ keyDuplicate: "models.key.duplicate",
89
+ expectedBoolean: "value.expected.boolean",
90
+ expectedString: "value.expected.string",
91
+ expectedList: "value.expected.list",
92
+ expectedInteger: "value.expected.integer",
93
+ expectedEnum: "value.expected.enum",
94
+ expectedModel: "value.expected.model",
95
+ nullUnsupported: "value.null.unsupported",
96
+ range: "value.range",
97
+ length: "value.length",
98
+ empty: "value.empty",
99
+ apiUnsupported: "value.api.unsupported",
100
+ imageDefaultNotAllowed: "imageGeneration.defaultModel.notAllowed",
101
+ } as const;
102
+
103
+ const CONTEXT_MODES = ["pi", "remote-compaction", "remote-windows"] as const;
104
+ const INPUT_SOURCES = ["legacy", "pi-context-hook"] as const;
105
+ const LEAVE_MANAGED_MODES = ["warn", "compact"] as const;
106
+ const SEARCH_ROUTES = ["unmanaged", "local", "hosted", "standalone-alpha"] as const;
107
+ const TRANSPORTS = ["standard", "codex-gateway"] as const;
108
+ const DIAGNOSTIC_LEVELS = ["error", "warn", "info", "debug"] as const;
109
+
110
+ /** v1 root sections; their presence next to `schemaVersion: 2` is a mixed-format document. */
111
+ const LEGACY_ROOT_KEYS = ["compaction", "webSearch", "imageGeneration", "autoMode"] as const;
112
+
113
+ const ROOT_KEYS = new Set(["$schema", "schemaVersion", "defaults", "models", "diagnostics"]);
114
+ const DEFAULTS_KEYS = new Set(["context", "webSearch", "imageGeneration", "responsesWebSocket", "autoMode"]);
115
+ const MODEL_OVERRIDE_KEYS = new Set(["context", "webSearch", "autoMode", "compatibility"]);
116
+ const CONTEXT_KEYS = new Set(["mode", "remoteCompaction", "nativeFallback", "remoteWindows"]);
117
+ const REMOTE_COMPACTION_KEYS = new Set(["model", "inputSource", "allowContinuityBreak", "apis"]);
118
+ const NATIVE_FALLBACK_KEYS = new Set(["enabled", "model", "thinkingLevel"]);
119
+ const REMOTE_WINDOWS_KEYS = new Set(["leaveManagedMode", "reminderThresholdPercent"]);
120
+ const WEB_SEARCH_KEYS = new Set(["route"]);
121
+ const IMAGE_GENERATION_KEYS = new Set(["enabled", "defaultModel", "allowedModels"]);
122
+ const RESPONSES_WEBSOCKET_KEYS = new Set(["enabled"]);
123
+ const COMPATIBILITY_KEYS = new Set(["transport"]);
124
+ const DIAGNOSTICS_KEYS = new Set([
125
+ "level",
126
+ "notifyOnLoad",
127
+ "captureRequests",
128
+ "captureResponses",
129
+ "redactSensitiveData",
130
+ "artifactRoot",
131
+ ]);
132
+
133
+ /** Features a `models[exactKey]` entry is able to own. */
134
+ const MODEL_OVERRIDABLE_FEATURES: readonly ConfigFeature[] = [
135
+ "context",
136
+ "webSearch",
137
+ "compatibility",
138
+ ];
139
+
140
+ /**
141
+ * Feature names usable as unknown-property attribution. `compaction` is the legacy name of the
142
+ * context section, so a v2 document that still uses it fails the owning feature closed instead
143
+ * of dropping the intent.
144
+ */
145
+ const FEATURE_NAMES: ReadonlyMap<string, ConfigFeature> = new Map<string, ConfigFeature>([
146
+ ["context", "context"],
147
+ ["compaction", "context"],
148
+ ["webSearch", "webSearch"],
149
+ ["imageGeneration", "imageGeneration"],
150
+ ["responsesWebSocket", "responsesWebSocket"],
151
+ ["compatibility", "compatibility"],
152
+ ["diagnostics", "diagnostics"],
153
+ ]);
154
+
155
+ /**
156
+ * Every effective leaf of `EffectiveToolkitPolicy`. Resolution fills `origins` for exactly these
157
+ * paths, so a read-only explanation can account for every value that reaches a consumer.
158
+ */
159
+ const POLICY_LEAVES: readonly { leaf: string; feature: ConfigFeature }[] = [
160
+ { leaf: "context.mode", feature: "context" },
161
+ { leaf: "context.remoteCompaction.model", feature: "context" },
162
+ { leaf: "context.remoteCompaction.inputSource", feature: "context" },
163
+ { leaf: "context.remoteCompaction.allowContinuityBreak", feature: "context" },
164
+ { leaf: "context.remoteCompaction.apis", feature: "context" },
165
+ { leaf: "context.nativeFallback.enabled", feature: "context" },
166
+ { leaf: "context.nativeFallback.model", feature: "context" },
167
+ { leaf: "context.nativeFallback.thinkingLevel", feature: "context" },
168
+ { leaf: "context.remoteWindows.leaveManagedMode", feature: "context" },
169
+ { leaf: "context.remoteWindows.reminderThresholdPercent", feature: "context" },
170
+ { leaf: "webSearch.route", feature: "webSearch" },
171
+ { leaf: "imageGeneration.enabled", feature: "imageGeneration" },
172
+ { leaf: "imageGeneration.defaultModel", feature: "imageGeneration" },
173
+ { leaf: "imageGeneration.allowedModels", feature: "imageGeneration" },
174
+ { leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
175
+ { leaf: "compatibility.transport", feature: "compatibility" },
176
+ { leaf: "diagnostics.level", feature: "diagnostics" },
177
+ { leaf: "diagnostics.notifyOnLoad", feature: "diagnostics" },
178
+ { leaf: "diagnostics.captureRequests", feature: "diagnostics" },
179
+ { leaf: "diagnostics.captureResponses", feature: "diagnostics" },
180
+ { leaf: "diagnostics.redactSensitiveData", feature: "diagnostics" },
181
+ { leaf: "diagnostics.artifactRoot", feature: "diagnostics" },
182
+ ];
183
+
184
+ /** Longest unrecognized key echoed into a diagnostic path. */
185
+ const MAX_DIAGNOSTIC_SEGMENT_CHARS = 64;
186
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
187
+ /** Characters that make a model key a pattern rather than an exact key. */
188
+ const MODEL_KEY_PATTERN_CHARACTERS = /[\s*?[\]{}]/;
189
+
190
+ function isRecord(value: unknown): value is Record<string, unknown> {
191
+ return !!value && typeof value === "object" && !Array.isArray(value);
192
+ }
193
+
194
+ /**
195
+ * Exact `provider/model-id` grammar shared with the legacy decoder: one separator, non-empty
196
+ * halves, no whitespace and no glob/bracket characters.
197
+ */
198
+ function isExactModelKey(value: string): boolean {
199
+ const separator = value.indexOf("/");
200
+ if (separator <= 0 || separator >= value.length - 1) return false;
201
+ return !MODEL_KEY_PATTERN_CHARACTERS.test(value) && !/[\u0000-\u001f\u007f]/.test(value);
202
+ }
203
+
204
+ /** Bound an unrecognized key before it reaches a diagnostic path; the text is never a value. */
205
+ function boundedSegment(value: string): string {
206
+ const cleaned = value.replace(CONTROL_CHARACTERS, "");
207
+ return cleaned.length > MAX_DIAGNOSTIC_SEGMENT_CHARS
208
+ ? `${cleaned.slice(0, MAX_DIAGNOSTIC_SEGMENT_CHARS)}…`
209
+ : cleaned;
210
+ }
211
+
212
+ function joinPath(prefix: string, segment: string): string {
213
+ return prefix ? `${prefix}.${segment}` : segment;
214
+ }
215
+
216
+ /** Input JSON path of a model scope, e.g. `models["openai/gpt-5.2"]`. */
217
+ function modelScopePath(key: string): string {
218
+ return `models[${JSON.stringify(key)}]`;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------------------------
222
+ // Document scopes
223
+ // ---------------------------------------------------------------------------------------------
224
+
225
+ type ScopeKind = "document" | "defaults" | "model";
226
+
227
+ type Scope = {
228
+ /** JSON path of the scope object inside the user document; `""` is the document root. */
229
+ input: string;
230
+ kind: ScopeKind;
231
+ /** Normalized exact key when `kind` is `"model"`. */
232
+ modelKey?: string;
233
+ };
234
+
235
+ const DOCUMENT_SCOPE: Scope = { input: "", kind: "document" };
236
+
237
+ function subScope(scope: Scope, key: string): Scope {
238
+ return { ...scope, input: joinPath(scope.input, key) };
239
+ }
240
+
241
+ type FieldDescriptor = {
242
+ /** Key inside the owning JSON object. */
243
+ key: string;
244
+ /** Effective policy leaf path, matching an entry in `POLICY_LEAVES`. */
245
+ leaf: string;
246
+ feature: ConfigFeature;
247
+ };
248
+
249
+ type LeafState = {
250
+ feature: ConfigFeature;
251
+ /** A present `defaults` value for this leaf failed validation. */
252
+ invalidAtDefaults: boolean;
253
+ /** A present override of the resolved model for this leaf failed validation. */
254
+ invalidAtModel: boolean;
255
+ /** A valid override of the resolved model supplied this leaf. */
256
+ modelApplied: boolean;
257
+ };
258
+
259
+ type ResolutionState = {
260
+ policy: EffectiveToolkitPolicy;
261
+ origins: Record<string, ConfigOrigin>;
262
+ issues: ConfigIssue[];
263
+ leaves: Map<string, LeafState>;
264
+ /** Feature-level errors a valid exact override cannot mask. */
265
+ hardInvalid: Set<ConfigFeature>;
266
+ /** Fatal document error: nothing is trusted and every feature is invalid. */
267
+ untrusted: boolean;
268
+ selectedKey: string | undefined;
269
+ gatewayCandidates: Set<string>;
270
+ };
271
+
272
+ function createState(modelKey: string | undefined): ResolutionState {
273
+ const origins: Record<string, ConfigOrigin> = {};
274
+ for (const { leaf } of POLICY_LEAVES) origins[leaf] = { kind: "builtin" };
275
+ const trimmed = typeof modelKey === "string" ? modelKey.trim() : "";
276
+ return {
277
+ policy: createPolicyDefaults(),
278
+ origins,
279
+ issues: [],
280
+ leaves: new Map(),
281
+ hardInvalid: new Set(),
282
+ gatewayCandidates: new Set(),
283
+ untrusted: false,
284
+ selectedKey: trimmed.length > 0 ? trimmed : undefined,
285
+ };
286
+ }
287
+
288
+ /** `defaults` and `diagnostics` always reach the resolved policy; a model scope only matches itself. */
289
+ function scopeApplies(state: ResolutionState, scope: Scope): boolean {
290
+ if (scope.kind === "model") return scope.modelKey === state.selectedKey;
291
+ return true;
292
+ }
293
+
294
+ function leafState(state: ResolutionState, leaf: string, feature: ConfigFeature): LeafState {
295
+ let entry = state.leaves.get(leaf);
296
+ if (!entry) {
297
+ entry = { feature, invalidAtDefaults: false, invalidAtModel: false, modelApplied: false };
298
+ state.leaves.set(leaf, entry);
299
+ }
300
+ return entry;
301
+ }
302
+
303
+ function withModel(scope: Scope, issue: ConfigIssue): ConfigIssue {
304
+ return scope.modelKey === undefined ? issue : { ...issue, modelKey: scope.modelKey };
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------------------------
308
+ // Diagnostics
309
+ // ---------------------------------------------------------------------------------------------
310
+
311
+ function fatalDocumentError(state: ResolutionState, code: string, path: string): void {
312
+ state.issues.push({ severity: "error", code, path, feature: "document" });
313
+ state.untrusted = true;
314
+ }
315
+
316
+ /** A value that is present but not usable for the leaf it was written to. */
317
+ function reportLeafError(
318
+ state: ResolutionState,
319
+ scope: Scope,
320
+ field: FieldDescriptor,
321
+ code: string,
322
+ inputPath: string,
323
+ ): void {
324
+ state.issues.push(
325
+ withModel(scope, { severity: "error", code, path: inputPath, feature: field.feature }),
326
+ );
327
+ if (!scopeApplies(state, scope)) return;
328
+ const leaf = leafState(state, field.leaf, field.feature);
329
+ if (scope.kind === "model") leaf.invalidAtModel = true;
330
+ else leaf.invalidAtDefaults = true;
331
+ }
332
+
333
+ /** A malformed object scope: the feature it owns cannot be trusted, override or not. */
334
+ function reportScopeError(
335
+ state: ResolutionState,
336
+ scope: Scope,
337
+ feature: ConfigFeature | "document",
338
+ code: string,
339
+ inputPath: string,
340
+ ): void {
341
+ state.issues.push(
342
+ withModel(scope, { severity: "error", code, path: inputPath, feature }),
343
+ );
344
+ if (scopeApplies(state, scope)) {
345
+ if (feature === "document") markModelOverrideInvalid(state, scope);
346
+ else state.hardInvalid.add(feature);
347
+ }
348
+ }
349
+
350
+ function markModelOverrideInvalid(state: ResolutionState, scope: Scope): void {
351
+ if (!scopeApplies(state, scope)) return;
352
+ for (const feature of MODEL_OVERRIDABLE_FEATURES) state.hardInvalid.add(feature);
353
+ }
354
+
355
+ /**
356
+ * Unknown properties are errors in their owning scope. A key named after a feature (or its
357
+ * legacy alias) fails that feature closed; otherwise the containing feature scope fails closed;
358
+ * an unmapped key blocks its containing model scope (or the document for a root/default key).
359
+ */
360
+ function reportUnknownKeys(
361
+ container: Record<string, unknown>,
362
+ known: ReadonlySet<string>,
363
+ scope: Scope,
364
+ state: ResolutionState,
365
+ owningFeature: ConfigFeature | undefined,
366
+ ): void {
367
+ for (const key of Object.keys(container)) {
368
+ if (known.has(key)) continue;
369
+ const feature = owningFeature ?? FEATURE_NAMES.get(key) ?? "document";
370
+ state.issues.push(
371
+ withModel(scope, {
372
+ severity: "error",
373
+ code: V2_ISSUE.keyUnknown,
374
+ path: joinPath(scope.input, "[unknown-field]"),
375
+ feature,
376
+ }),
377
+ );
378
+ if (scopeApplies(state, scope)) {
379
+ if (feature !== "document") state.hardInvalid.add(feature);
380
+ else if (scope.kind === "model") markModelOverrideInvalid(state, scope);
381
+ else state.untrusted = true;
382
+ }
383
+ }
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------------------------
387
+ // Value readers
388
+ // ---------------------------------------------------------------------------------------------
389
+
390
+ type ReadResult<T> = { ok: true; value: T } | { ok: false; code: string };
391
+
392
+ function typeCode(value: unknown, expected: string): string {
393
+ return value === null ? V2_ISSUE.nullUnsupported : expected;
394
+ }
395
+
396
+ function readBoolean(value: unknown): ReadResult<boolean> {
397
+ return typeof value === "boolean"
398
+ ? { ok: true, value }
399
+ : { ok: false, code: typeCode(value, V2_ISSUE.expectedBoolean) };
400
+ }
401
+
402
+ function readEnum<T extends string>(allowed: readonly T[], value: unknown): ReadResult<T> {
403
+ if (typeof value !== "string") {
404
+ return { ok: false, code: typeCode(value, V2_ISSUE.expectedString) };
405
+ }
406
+ const normalized = value.trim();
407
+ for (const entry of allowed) {
408
+ if (entry === normalized) return { ok: true, value: entry };
409
+ }
410
+ return { ok: false, code: V2_ISSUE.expectedEnum };
411
+ }
412
+
413
+ function readInteger(value: unknown, min: number, max: number): ReadResult<number> {
414
+ if (typeof value !== "number") {
415
+ return { ok: false, code: typeCode(value, V2_ISSUE.expectedInteger) };
416
+ }
417
+ if (!Number.isInteger(value) || value < min || value > max) {
418
+ return { ok: false, code: V2_ISSUE.range };
419
+ }
420
+ return { ok: true, value };
421
+ }
422
+
423
+ function readThinkingLevel(value: unknown): ReadResult<ThinkingLevel> {
424
+ return readEnum(THINKING_LEVELS, value);
425
+ }
426
+
427
+ /** A non-empty, trimmed string such as `artifactRoot`. */
428
+ function readNonEmptyString(value: unknown, maxChars?: number): ReadResult<string> {
429
+ if (typeof value !== "string") {
430
+ return { ok: false, code: typeCode(value, V2_ISSUE.expectedString) };
431
+ }
432
+ const normalized = value.trim();
433
+ if (normalized.length === 0) return { ok: false, code: V2_ISSUE.empty };
434
+ if (maxChars !== undefined && normalized.length > maxChars) {
435
+ return { ok: false, code: V2_ISSUE.length };
436
+ }
437
+ return { ok: true, value: normalized };
438
+ }
439
+
440
+ /** Exact `provider/model-id` reference; `null` clears an inherited reference. */
441
+ function readModelRef(value: unknown): ReadResult<string | null> {
442
+ if (value === null) return { ok: true, value: null };
443
+ if (typeof value !== "string") {
444
+ return { ok: false, code: V2_ISSUE.expectedModel };
445
+ }
446
+ const normalized = value.trim();
447
+ if (normalized.length === 0) return { ok: false, code: V2_ISSUE.empty };
448
+ if (!isExactModelKey(normalized)) return { ok: false, code: V2_ISSUE.expectedModel };
449
+ return { ok: true, value: normalized };
450
+ }
451
+
452
+ /** Order-preserving string list with the legacy trim/dedupe/drop-blank normalization. */
453
+ function readStringList(
454
+ value: unknown,
455
+ options?: { maxEntryChars?: number; requireNonEmpty?: boolean },
456
+ ): ReadResult<string[]> {
457
+ if (!Array.isArray(value)) {
458
+ return { ok: false, code: typeCode(value, V2_ISSUE.expectedList) };
459
+ }
460
+ const entries: string[] = [];
461
+ for (const item of value) {
462
+ if (typeof item !== "string") {
463
+ return { ok: false, code: V2_ISSUE.expectedString };
464
+ }
465
+ const normalized = item.trim();
466
+ if (normalized.length === 0) continue;
467
+ if (options?.maxEntryChars !== undefined && normalized.length > options.maxEntryChars) {
468
+ return { ok: false, code: V2_ISSUE.length };
469
+ }
470
+ if (!entries.includes(normalized)) entries.push(normalized);
471
+ }
472
+ if (options?.requireNonEmpty && entries.length === 0) {
473
+ return { ok: false, code: V2_ISSUE.empty };
474
+ }
475
+ return { ok: true, value: entries };
476
+ }
477
+
478
+ function readImageModelList(value: unknown): ReadResult<string[]> {
479
+ return readStringList(value, {
480
+ maxEntryChars: MAX_IMAGE_MODEL_ID_CHARS,
481
+ requireNonEmpty: true,
482
+ });
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------------------------
486
+ // Field application
487
+ // ---------------------------------------------------------------------------------------------
488
+
489
+ /**
490
+ * Apply one known leaf when it is present: validate, and on success write the effective value,
491
+ * record its origin and mark the leaf supplied for the resolved model. An invalid present value
492
+ * is reported and leaves the previously resolved value and origin untouched.
493
+ */
494
+ function applyField<T>(
495
+ container: Record<string, unknown>,
496
+ field: FieldDescriptor,
497
+ scope: Scope,
498
+ state: ResolutionState,
499
+ read: (value: unknown) => ReadResult<T>,
500
+ assign: (value: T) => void,
501
+ ): void {
502
+ if (!Object.hasOwn(container, field.key)) return;
503
+ const inputPath = joinPath(scope.input, field.key);
504
+ const result = read(container[field.key]);
505
+ if (!result.ok) {
506
+ reportLeafError(state, scope, field, result.code, inputPath);
507
+ return;
508
+ }
509
+ if (!scopeApplies(state, scope)) return;
510
+ assign(result.value);
511
+ state.origins[field.leaf] = {
512
+ kind: scope.kind === "defaults" ? "defaults" : "model",
513
+ path: inputPath,
514
+ };
515
+ if (scope.kind === "model") leafState(state, field.leaf, field.feature).modelApplied = true;
516
+ }
517
+
518
+ /** Open a nested known object; a present non-object is a scope error for the owning feature. */
519
+ function openGroup(
520
+ container: Record<string, unknown>,
521
+ key: string,
522
+ scope: Scope,
523
+ state: ResolutionState,
524
+ feature: ConfigFeature,
525
+ ): { raw: Record<string, unknown>; scope: Scope } | undefined {
526
+ if (!Object.hasOwn(container, key)) return undefined;
527
+ const value = container[key];
528
+ if (!isRecord(value)) {
529
+ reportScopeError(state, scope, feature, V2_ISSUE.scopeInvalid, joinPath(scope.input, key));
530
+ return undefined;
531
+ }
532
+ return { raw: value, scope: subScope(scope, key) };
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------------------------
536
+ // Feature scopes
537
+ // ---------------------------------------------------------------------------------------------
538
+
539
+ function applyContextSettings(
540
+ raw: Record<string, unknown>,
541
+ scope: Scope,
542
+ state: ResolutionState,
543
+ ): void {
544
+ reportUnknownKeys(raw, CONTEXT_KEYS, scope, state, "context");
545
+
546
+ applyField(
547
+ raw,
548
+ { key: "mode", leaf: "context.mode", feature: "context" },
549
+ scope,
550
+ state,
551
+ (value) => readEnum(CONTEXT_MODES, value),
552
+ (value) => {
553
+ state.policy.context.mode = value;
554
+ },
555
+ );
556
+
557
+ const remoteCompaction = openGroup(raw, "remoteCompaction", scope, state, "context");
558
+ if (remoteCompaction) applyRemoteCompactionSettings(remoteCompaction.raw, remoteCompaction.scope, state);
559
+
560
+ const nativeFallback = openGroup(raw, "nativeFallback", scope, state, "context");
561
+ if (nativeFallback) applyNativeFallbackSettings(nativeFallback.raw, nativeFallback.scope, state);
562
+
563
+ const remoteWindows = openGroup(raw, "remoteWindows", scope, state, "context");
564
+ if (remoteWindows) applyRemoteWindowsSettings(remoteWindows.raw, remoteWindows.scope, state);
565
+ }
566
+
567
+ function applyRemoteCompactionSettings(
568
+ raw: Record<string, unknown>,
569
+ scope: Scope,
570
+ state: ResolutionState,
571
+ ): void {
572
+ reportUnknownKeys(raw, REMOTE_COMPACTION_KEYS, scope, state, "context");
573
+
574
+ applyField(
575
+ raw,
576
+ { key: "model", leaf: "context.remoteCompaction.model", feature: "context" },
577
+ scope,
578
+ state,
579
+ readModelRef,
580
+ (value) => {
581
+ state.policy.context.remoteCompaction.model = value;
582
+ },
583
+ );
584
+ applyField(
585
+ raw,
586
+ { key: "inputSource", leaf: "context.remoteCompaction.inputSource", feature: "context" },
587
+ scope,
588
+ state,
589
+ (value) => readEnum(INPUT_SOURCES, value),
590
+ (value) => {
591
+ state.policy.context.remoteCompaction.inputSource = value;
592
+ },
593
+ );
594
+ applyField(
595
+ raw,
596
+ {
597
+ key: "allowContinuityBreak",
598
+ leaf: "context.remoteCompaction.allowContinuityBreak",
599
+ feature: "context",
600
+ },
601
+ scope,
602
+ state,
603
+ readBoolean,
604
+ (value) => {
605
+ state.policy.context.remoteCompaction.allowContinuityBreak = value;
606
+ },
607
+ );
608
+ applyApisField(raw, scope, state);
609
+ }
610
+
611
+ /**
612
+ * `apis` may only narrow the Responses APIs the toolkit can build a compact URL for, like the
613
+ * legacy decoder. Unsupported entries invalidate this leaf; a malformed selected policy
614
+ * cannot silently fall back to the inherited API list.
615
+ */
616
+ function applyApisField(raw: Record<string, unknown>, scope: Scope, state: ResolutionState): void {
617
+ if (!Object.hasOwn(raw, "apis")) return;
618
+ const field: FieldDescriptor = {
619
+ key: "apis",
620
+ leaf: "context.remoteCompaction.apis",
621
+ feature: "context",
622
+ };
623
+ const inputPath = joinPath(scope.input, "apis");
624
+ const value = raw.apis;
625
+ if (!Array.isArray(value)) {
626
+ reportLeafError(state, scope, field, typeCode(value, V2_ISSUE.expectedList), inputPath);
627
+ return;
628
+ }
629
+
630
+ const accepted: string[] = [];
631
+ for (let index = 0; index < value.length; index += 1) {
632
+ const entry = value[index];
633
+ if (typeof entry !== "string") {
634
+ reportLeafError(state, scope, field, V2_ISSUE.expectedString, inputPath);
635
+ return;
636
+ }
637
+ const normalized = entry.trim();
638
+ if (normalized.length === 0) continue;
639
+ const capable = RESPONSES_COMPACT_CAPABLE_APIS as readonly string[];
640
+ if (!capable.includes(normalized)) {
641
+ reportLeafError(state, scope, field, V2_ISSUE.apiUnsupported, `${inputPath}[${index}]`);
642
+ return;
643
+ }
644
+ if (!accepted.includes(normalized)) accepted.push(normalized);
645
+ }
646
+
647
+ if (!scopeApplies(state, scope)) return;
648
+ state.policy.context.remoteCompaction.apis = accepted;
649
+ state.origins[field.leaf] = {
650
+ kind: scope.kind === "defaults" ? "defaults" : "model",
651
+ path: inputPath,
652
+ };
653
+ if (scope.kind === "model") leafState(state, field.leaf, field.feature).modelApplied = true;
654
+ }
655
+
656
+ function applyNativeFallbackSettings(
657
+ raw: Record<string, unknown>,
658
+ scope: Scope,
659
+ state: ResolutionState,
660
+ ): void {
661
+ reportUnknownKeys(raw, NATIVE_FALLBACK_KEYS, scope, state, "context");
662
+
663
+ applyField(
664
+ raw,
665
+ { key: "enabled", leaf: "context.nativeFallback.enabled", feature: "context" },
666
+ scope,
667
+ state,
668
+ readBoolean,
669
+ (value) => {
670
+ state.policy.context.nativeFallback.enabled = value;
671
+ },
672
+ );
673
+ applyField(
674
+ raw,
675
+ { key: "model", leaf: "context.nativeFallback.model", feature: "context" },
676
+ scope,
677
+ state,
678
+ readModelRef,
679
+ (value) => {
680
+ state.policy.context.nativeFallback.model = value;
681
+ },
682
+ );
683
+ applyField(
684
+ raw,
685
+ { key: "thinkingLevel", leaf: "context.nativeFallback.thinkingLevel", feature: "context" },
686
+ scope,
687
+ state,
688
+ readThinkingLevel,
689
+ (value) => {
690
+ state.policy.context.nativeFallback.thinkingLevel = value;
691
+ },
692
+ );
693
+ }
694
+
695
+ function applyRemoteWindowsSettings(
696
+ raw: Record<string, unknown>,
697
+ scope: Scope,
698
+ state: ResolutionState,
699
+ ): void {
700
+ reportUnknownKeys(raw, REMOTE_WINDOWS_KEYS, scope, state, "context");
701
+
702
+ applyField(
703
+ raw,
704
+ { key: "leaveManagedMode", leaf: "context.remoteWindows.leaveManagedMode", feature: "context" },
705
+ scope,
706
+ state,
707
+ (value) => readEnum(LEAVE_MANAGED_MODES, value),
708
+ (value) => {
709
+ state.policy.context.remoteWindows.leaveManagedMode = value;
710
+ },
711
+ );
712
+ applyField(
713
+ raw,
714
+ {
715
+ key: "reminderThresholdPercent",
716
+ leaf: "context.remoteWindows.reminderThresholdPercent",
717
+ feature: "context",
718
+ },
719
+ scope,
720
+ state,
721
+ (value) => readInteger(value, 0, 100),
722
+ (value) => {
723
+ state.policy.context.remoteWindows.reminderThresholdPercent = value;
724
+ },
725
+ );
726
+ }
727
+
728
+ function applyWebSearchSettings(
729
+ raw: Record<string, unknown>,
730
+ scope: Scope,
731
+ state: ResolutionState,
732
+ ): void {
733
+ reportUnknownKeys(raw, WEB_SEARCH_KEYS, scope, state, "webSearch");
734
+
735
+ applyField(
736
+ raw,
737
+ { key: "route", leaf: "webSearch.route", feature: "webSearch" },
738
+ scope,
739
+ state,
740
+ (value) => readEnum(SEARCH_ROUTES, value),
741
+ (value) => {
742
+ state.policy.webSearch.route = value satisfies SearchRoute;
743
+ },
744
+ );
745
+ }
746
+
747
+ /**
748
+ * Image output settings are global only. The effective default must be a member of the effective
749
+ * non-empty allowed list; a mismatch or an empty list fails the paid feature closed instead of
750
+ * selecting a different model.
751
+ */
752
+ function applyImageGenerationSettings(
753
+ raw: Record<string, unknown>,
754
+ scope: Scope,
755
+ state: ResolutionState,
756
+ ): void {
757
+ reportUnknownKeys(raw, IMAGE_GENERATION_KEYS, scope, state, "imageGeneration");
758
+
759
+ applyField(
760
+ raw,
761
+ { key: "enabled", leaf: "imageGeneration.enabled", feature: "imageGeneration" },
762
+ scope,
763
+ state,
764
+ readBoolean,
765
+ (value) => {
766
+ state.policy.imageGeneration.enabled = value;
767
+ },
768
+ );
769
+ applyField(
770
+ raw,
771
+ { key: "defaultModel", leaf: "imageGeneration.defaultModel", feature: "imageGeneration" },
772
+ scope,
773
+ state,
774
+ (value) => readNonEmptyString(value, MAX_IMAGE_MODEL_ID_CHARS),
775
+ (value) => {
776
+ state.policy.imageGeneration.defaultModel = value;
777
+ },
778
+ );
779
+ applyField(
780
+ raw,
781
+ { key: "allowedModels", leaf: "imageGeneration.allowedModels", feature: "imageGeneration" },
782
+ scope,
783
+ state,
784
+ readImageModelList,
785
+ (value) => {
786
+ state.policy.imageGeneration.allowedModels = value;
787
+ },
788
+ );
789
+
790
+ const defaultLeaf = state.leaves.get("imageGeneration.defaultModel");
791
+ const allowedLeaf = state.leaves.get("imageGeneration.allowedModels");
792
+ const unreliable = (leaf: LeafState | undefined): boolean =>
793
+ !!leaf && (leaf.invalidAtDefaults || leaf.invalidAtModel);
794
+ if (unreliable(defaultLeaf) || unreliable(allowedLeaf)) return;
795
+
796
+ const image = state.policy.imageGeneration;
797
+ if (image.allowedModels.includes(image.defaultModel)) return;
798
+ reportScopeError(
799
+ state,
800
+ scope,
801
+ "imageGeneration",
802
+ V2_ISSUE.imageDefaultNotAllowed,
803
+ joinPath(scope.input, "defaultModel"),
804
+ );
805
+ }
806
+
807
+ function applyResponsesWebSocketSettings(
808
+ raw: Record<string, unknown>,
809
+ scope: Scope,
810
+ state: ResolutionState,
811
+ ): void {
812
+ reportUnknownKeys(raw, RESPONSES_WEBSOCKET_KEYS, scope, state, "responsesWebSocket");
813
+
814
+ applyField(
815
+ raw,
816
+ { key: "enabled", leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
817
+ scope,
818
+ state,
819
+ readBoolean,
820
+ (value) => {
821
+ state.policy.responsesWebSocket.enabled = value;
822
+ },
823
+ );
824
+ }
825
+
826
+ /**
827
+ * A retired `autoMode` section is recognized only to explain its removal. Its value is never read
828
+ * or validated, whatever shape it has, so an old reviewer/classifier/breaker configuration cannot
829
+ * invalidate another feature, and no user-supplied content is copied into a diagnostic.
830
+ */
831
+ function reportRetiredAutoMode(scope: Scope, state: ResolutionState): void {
832
+ state.issues.push(
833
+ withModel(scope, {
834
+ severity: "warning",
835
+ code: RETIRED_AUTO_MODE_ISSUE,
836
+ path: joinPath(scope.input, "autoMode"),
837
+ feature: "document",
838
+ }),
839
+ );
840
+ }
841
+
842
+ /** Protocol compatibility is an exact-model opt-in; there is no broad defaults wildcard. */
843
+ function applyCompatibilitySettings(
844
+ raw: Record<string, unknown>,
845
+ scope: Scope,
846
+ state: ResolutionState,
847
+ ): void {
848
+ reportUnknownKeys(raw, COMPATIBILITY_KEYS, scope, state, "compatibility");
849
+ if (scope.modelKey && Object.hasOwn(raw, "transport") && typeof raw.transport === "string" && raw.transport.trim() === "codex-gateway") {
850
+ state.gatewayCandidates.add(scope.modelKey);
851
+ }
852
+
853
+ applyField(
854
+ raw,
855
+ { key: "transport", leaf: "compatibility.transport", feature: "compatibility" },
856
+ scope,
857
+ state,
858
+ (value) => readEnum(TRANSPORTS, value),
859
+ (value) => {
860
+ state.policy.compatibility.transport = value;
861
+ },
862
+ );
863
+ }
864
+
865
+ function applyDiagnosticsSettings(
866
+ raw: Record<string, unknown>,
867
+ scope: Scope,
868
+ state: ResolutionState,
869
+ ): void {
870
+ reportUnknownKeys(raw, DIAGNOSTICS_KEYS, scope, state, "diagnostics");
871
+
872
+ applyField(
873
+ raw,
874
+ { key: "level", leaf: "diagnostics.level", feature: "diagnostics" },
875
+ scope,
876
+ state,
877
+ (value) => readEnum(DIAGNOSTIC_LEVELS, value),
878
+ (value) => {
879
+ state.policy.diagnostics.level = value;
880
+ },
881
+ );
882
+ applyField(
883
+ raw,
884
+ { key: "notifyOnLoad", leaf: "diagnostics.notifyOnLoad", feature: "diagnostics" },
885
+ scope,
886
+ state,
887
+ readBoolean,
888
+ (value) => {
889
+ state.policy.diagnostics.notifyOnLoad = value;
890
+ },
891
+ );
892
+ applyField(
893
+ raw,
894
+ { key: "captureRequests", leaf: "diagnostics.captureRequests", feature: "diagnostics" },
895
+ scope,
896
+ state,
897
+ readBoolean,
898
+ (value) => {
899
+ state.policy.diagnostics.captureRequests = value;
900
+ },
901
+ );
902
+ applyField(
903
+ raw,
904
+ { key: "captureResponses", leaf: "diagnostics.captureResponses", feature: "diagnostics" },
905
+ scope,
906
+ state,
907
+ readBoolean,
908
+ (value) => {
909
+ state.policy.diagnostics.captureResponses = value;
910
+ },
911
+ );
912
+ applyField(
913
+ raw,
914
+ {
915
+ key: "redactSensitiveData",
916
+ leaf: "diagnostics.redactSensitiveData",
917
+ feature: "diagnostics",
918
+ },
919
+ scope,
920
+ state,
921
+ readBoolean,
922
+ (value) => {
923
+ state.policy.diagnostics.redactSensitiveData = value;
924
+ },
925
+ );
926
+ applyField(
927
+ raw,
928
+ { key: "artifactRoot", leaf: "diagnostics.artifactRoot", feature: "diagnostics" },
929
+ scope,
930
+ state,
931
+ readNonEmptyString,
932
+ (value) => {
933
+ // Path expansion stays in the loader; the resolver records the configured value.
934
+ state.policy.diagnostics.artifactRoot = value;
935
+ },
936
+ );
937
+ }
938
+
939
+ /** Reusable scope appliers so `defaults.<feature>` and `models[key].<feature>` share one contract. */
940
+ function applyContextScope(container: Record<string, unknown>, scope: Scope, state: ResolutionState): void {
941
+ const group = openGroup(container, "context", scope, state, "context");
942
+ if (group) applyContextSettings(group.raw, group.scope, state);
943
+ }
944
+
945
+ function applyWebSearchScope(
946
+ container: Record<string, unknown>,
947
+ scope: Scope,
948
+ state: ResolutionState,
949
+ ): void {
950
+ const group = openGroup(container, "webSearch", scope, state, "webSearch");
951
+ if (group) applyWebSearchSettings(group.raw, group.scope, state);
952
+ }
953
+
954
+ function applyImageGenerationScope(
955
+ container: Record<string, unknown>,
956
+ scope: Scope,
957
+ state: ResolutionState,
958
+ ): void {
959
+ const group = openGroup(container, "imageGeneration", scope, state, "imageGeneration");
960
+ if (group) applyImageGenerationSettings(group.raw, group.scope, state);
961
+ }
962
+
963
+ function applyResponsesWebSocketScope(
964
+ container: Record<string, unknown>,
965
+ scope: Scope,
966
+ state: ResolutionState,
967
+ ): void {
968
+ const group = openGroup(container, "responsesWebSocket", scope, state, "responsesWebSocket");
969
+ if (group) applyResponsesWebSocketSettings(group.raw, group.scope, state);
970
+ }
971
+
972
+ function applyAutoModeScope(
973
+ container: Record<string, unknown>,
974
+ scope: Scope,
975
+ state: ResolutionState,
976
+ ): void {
977
+ if (Object.hasOwn(container, "autoMode")) reportRetiredAutoMode(scope, state);
978
+ }
979
+
980
+ function applyCompatibilityScope(
981
+ container: Record<string, unknown>,
982
+ scope: Scope,
983
+ state: ResolutionState,
984
+ ): void {
985
+ const group = openGroup(container, "compatibility", scope, state, "compatibility");
986
+ if (group) applyCompatibilitySettings(group.raw, group.scope, state);
987
+ }
988
+
989
+ // ---------------------------------------------------------------------------------------------
990
+ // Document sections
991
+ // ---------------------------------------------------------------------------------------------
992
+
993
+ function applyDefaults(doc: Record<string, unknown>, state: ResolutionState): void {
994
+ if (!Object.hasOwn(doc, "defaults")) return;
995
+ const raw = doc.defaults;
996
+ if (!isRecord(raw)) {
997
+ state.issues.push({
998
+ severity: "error",
999
+ code: V2_ISSUE.defaultsInvalid,
1000
+ path: "defaults",
1001
+ feature: "document",
1002
+ });
1003
+ state.untrusted = true;
1004
+ return;
1005
+ }
1006
+
1007
+ const scope: Scope = { input: "defaults", kind: "defaults" };
1008
+ reportUnknownKeys(raw, DEFAULTS_KEYS, scope, state, undefined);
1009
+ applyContextScope(raw, scope, state);
1010
+ applyWebSearchScope(raw, scope, state);
1011
+ applyImageGenerationScope(raw, scope, state);
1012
+ applyResponsesWebSocketScope(raw, scope, state);
1013
+ applyAutoModeScope(raw, scope, state);
1014
+ }
1015
+
1016
+ function applyModels(doc: Record<string, unknown>, state: ResolutionState): void {
1017
+ if (!Object.hasOwn(doc, "models")) return;
1018
+ const raw = doc.models;
1019
+ if (!isRecord(raw)) {
1020
+ state.issues.push({
1021
+ severity: "error",
1022
+ code: V2_ISSUE.modelsInvalid,
1023
+ path: "models",
1024
+ feature: "document",
1025
+ });
1026
+ markModelOverrideInvalid(state, { input: "models", kind: "document" });
1027
+ return;
1028
+ }
1029
+
1030
+ // Trim-normalize keys and reject collisions instead of resolving them last-wins.
1031
+ // Keep the raw key for JSON paths: normalization selects the scope, but provenance and
1032
+ // diagnostics must still point at the property the caller actually supplied.
1033
+ const normalized = new Map<string, string[]>();
1034
+ for (const rawKey of Object.keys(raw)) {
1035
+ const key = rawKey.trim();
1036
+ const existing = normalized.get(key);
1037
+ if (existing) existing.push(rawKey);
1038
+ else normalized.set(key, [rawKey]);
1039
+ }
1040
+
1041
+ for (const [key, rawKeys] of normalized) {
1042
+ const firstRawKey = rawKeys[0] as string;
1043
+ const scope: Scope = { input: modelScopePath(firstRawKey), kind: "model", modelKey: key };
1044
+ if (!isExactModelKey(key)) {
1045
+ state.issues.push({
1046
+ severity: "error",
1047
+ code: V2_ISSUE.keyInvalid,
1048
+ path: modelScopePath(boundedSegment(firstRawKey)),
1049
+ feature: "document",
1050
+ });
1051
+ if (key === state.selectedKey) markModelOverrideInvalid(state, scope);
1052
+ continue;
1053
+ }
1054
+ if (rawKeys.length > 1) {
1055
+ state.issues.push(
1056
+ withModel(scope, {
1057
+ severity: "error",
1058
+ code: V2_ISSUE.keyDuplicate,
1059
+ path: modelScopePath(boundedSegment(firstRawKey)),
1060
+ feature: "document",
1061
+ }),
1062
+ );
1063
+ if (key === state.selectedKey) markModelOverrideInvalid(state, scope);
1064
+ continue;
1065
+ }
1066
+
1067
+ const entry = Object.hasOwn(raw, firstRawKey) ? raw[firstRawKey] : undefined;
1068
+ if (!isRecord(entry)) {
1069
+ reportScopeError(state, scope, "document", V2_ISSUE.scopeInvalid, modelScopePath(boundedSegment(firstRawKey)));
1070
+ markModelOverrideInvalid(state, scope);
1071
+ continue;
1072
+ }
1073
+
1074
+ reportUnknownKeys(entry, MODEL_OVERRIDE_KEYS, scope, state, undefined);
1075
+ applyContextScope(entry, scope, state);
1076
+ applyWebSearchScope(entry, scope, state);
1077
+ applyAutoModeScope(entry, scope, state);
1078
+ applyCompatibilityScope(entry, scope, state);
1079
+ }
1080
+ }
1081
+
1082
+ function applyDiagnostics(doc: Record<string, unknown>, state: ResolutionState): void {
1083
+ if (!Object.hasOwn(doc, "diagnostics")) return;
1084
+ const raw = doc.diagnostics;
1085
+ if (!isRecord(raw)) {
1086
+ reportScopeError(
1087
+ state,
1088
+ { input: "diagnostics", kind: "document" },
1089
+ "diagnostics",
1090
+ V2_ISSUE.diagnosticsInvalid,
1091
+ "diagnostics",
1092
+ );
1093
+ return;
1094
+ }
1095
+ // Diagnostics are global, so they resolve into the inherited policy like `defaults`.
1096
+ applyDiagnosticsSettings(raw, { input: "diagnostics", kind: "defaults" }, state);
1097
+ }
1098
+
1099
+ type V2Resolution = ResolvedToolkitPolicy & { gatewayModelKeys: string[] };
1100
+
1101
+ function finish(state: ResolutionState): V2Resolution {
1102
+ const invalidFeatures = CONFIG_FEATURES.filter((feature) => {
1103
+ if (state.untrusted || state.hardInvalid.has(feature)) return true;
1104
+ for (const leaf of state.leaves.values()) {
1105
+ if (leaf.feature !== feature) continue;
1106
+ if (leaf.invalidAtModel) return true;
1107
+ if (leaf.invalidAtDefaults && !leaf.modelApplied) return true;
1108
+ }
1109
+ return false;
1110
+ });
1111
+ // All model scopes were validated in the same pass. An unrelated context/search error
1112
+ // does not change a valid transport; document/compatibility errors do.
1113
+ const blockedGateways = new Set<string>();
1114
+ let blockAllGateways = state.untrusted;
1115
+ for (const issue of state.issues) {
1116
+ if (issue.severity !== "error" || (issue.feature !== "document" && issue.feature !== "compatibility")) continue;
1117
+ if (issue.modelKey !== undefined) blockedGateways.add(issue.modelKey);
1118
+ else if (issue.code !== V2_ISSUE.keyInvalid) blockAllGateways = true;
1119
+ }
1120
+ return {
1121
+ policy: state.policy,
1122
+ origins: state.origins,
1123
+ issues: state.issues,
1124
+ invalidFeatures: [...invalidFeatures],
1125
+ gatewayModelKeys: blockAllGateways ? [] : [...state.gatewayCandidates].filter((key) => !blockedGateways.has(key)),
1126
+ };
1127
+ }
1128
+
1129
+ /**
1130
+ * Validate and resolve a version 2 document for `modelKey`.
1131
+ *
1132
+ * Pure: no file I/O, path expansion, auth lookup, provider call or tool mutation. Callers must
1133
+ * route unversioned documents through the legacy adapter instead. Consumers must treat any
1134
+ * feature listed in `invalidFeatures` as fail-closed and must never reconstruct the merge
1135
+ * themselves.
1136
+ */
1137
+ export function resolveV2Config(raw: unknown, modelKey: string | undefined): V2Resolution {
1138
+ const state = createState(modelKey);
1139
+
1140
+ if (!isRecord(raw)) {
1141
+ fatalDocumentError(state, V2_ISSUE.rootInvalid, "document");
1142
+ return finish(state);
1143
+ }
1144
+ if (!Object.hasOwn(raw, "schemaVersion")) {
1145
+ fatalDocumentError(state, V2_ISSUE.schemaVersionMissing, "schemaVersion");
1146
+ return finish(state);
1147
+ }
1148
+ if (raw.schemaVersion !== 2) {
1149
+ fatalDocumentError(state, V2_ISSUE.schemaVersionUnsupported, "schemaVersion");
1150
+ return finish(state);
1151
+ }
1152
+ if (LEGACY_ROOT_KEYS.some((key) => Object.hasOwn(raw, key))) {
1153
+ fatalDocumentError(state, V2_ISSUE.formatMixed, "document");
1154
+ return finish(state);
1155
+ }
1156
+
1157
+ if (Object.hasOwn(raw, "$schema") && typeof raw.$schema !== "string") {
1158
+ fatalDocumentError(state, V2_ISSUE.schemaInvalid, "$schema");
1159
+ }
1160
+ reportUnknownKeys(raw, ROOT_KEYS, DOCUMENT_SCOPE, state, undefined);
1161
+
1162
+ applyDefaults(raw, state);
1163
+ applyModels(raw, state);
1164
+ applyDiagnostics(raw, state);
1165
+
1166
+ return finish(state);
1167
+ }