@bitkyc08/opencodex 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -14,6 +14,7 @@ import type {
14
14
  import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
15
15
  import { listComboIds, resolveComboId } from "../combos";
16
16
  import { hasOwnProvider } from "../config";
17
+ import { MAX_COMPATIBILITY_REQUIRED_SUITES } from "./compatibility/types";
17
18
  import { POLICY_NAMESPACE } from "./profile-namespace";
18
19
 
19
20
  export { POLICY_NAMESPACE };
@@ -36,6 +37,17 @@ export const DEFAULT_UNKNOWN_EVIDENCE: Record<"capability" | "health" | "quota"
36
37
  cost: "penalize",
37
38
  };
38
39
 
40
+ export const DEFAULT_DEGRADED_EVIDENCE: OcxRoutingUnknownEvidenceMode = "penalize";
41
+ export const DEFAULT_COMPATIBILITY_UNKNOWN_EVIDENCE: OcxRoutingUnknownEvidenceMode = "exclude";
42
+
43
+ export interface NormalizedRoutingProfileCompatibility {
44
+ requiredSuites: Array<{ suiteId: string; evidenceLayer: "protocol_conformance" | "live_route_compatibility" }>;
45
+ minStatus?: "PROBED" | "VERIFIED";
46
+ maxEvidenceAgeMs?: number;
47
+ unknownEvidence: OcxRoutingUnknownEvidenceMode;
48
+ degradedEvidence: OcxRoutingUnknownEvidenceMode;
49
+ }
50
+
39
51
  export interface RoutingProfileValidationIssue {
40
52
  path: Array<string | number>;
41
53
  message: string;
@@ -63,6 +75,7 @@ export interface NormalizedRoutingProfile {
63
75
  optimize: { latency: number; health: number; cost: number; quota: number };
64
76
  limits: { maxEstimatedCostUsd?: number; onUnknownCost?: OcxRoutingUnknownCostCapMode };
65
77
  unknownEvidence: Record<"capability" | "health" | "quota" | "cost", OcxRoutingUnknownEvidenceMode>;
78
+ compatibility?: NormalizedRoutingProfileCompatibility;
66
79
  revision: string;
67
80
  }
68
81
 
@@ -80,6 +93,7 @@ const REQUIRE_KEYS = [
80
93
  ] as const;
81
94
 
82
95
  const UNKNOWN_EVIDENCE_KEYS = ["capability", "health", "quota", "cost"] as const;
96
+ const EVIDENCE_LAYERS = new Set(["protocol_conformance", "live_route_compatibility"]);
83
97
 
84
98
  export function isValidPolicyId(id: string): boolean {
85
99
  return POLICY_ID_PATTERN.test(id);
@@ -153,8 +167,6 @@ function aliasIssues(
153
167
  message: "bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed",
154
168
  });
155
169
  }
156
- // Cross-namespace collisions: providers, combos, account namespaces, and
157
- // sibling profile aliases all own public model ids that must stay unique.
158
170
  if (hasOwnProvider(config.providers, alias)) {
159
171
  issues.push({ path: ["alias"], message: `alias "${alias}" collides with configured provider name "${alias}"` });
160
172
  }
@@ -342,6 +354,72 @@ export function routingProfileIssues(
342
354
  }
343
355
  }
344
356
 
357
+ if (body.compatibility !== undefined) {
358
+ if (!body.compatibility || typeof body.compatibility !== "object" || Array.isArray(body.compatibility)) {
359
+ issues.push({ path: ["compatibility"], message: "compatibility must be an object" });
360
+ } else {
361
+ const compatibility = body.compatibility as Record<string, unknown>;
362
+ if (compatibility.requiredSuites !== undefined) {
363
+ if (!Array.isArray(compatibility.requiredSuites)) {
364
+ issues.push({ path: ["compatibility", "requiredSuites"], message: "requiredSuites must be an array" });
365
+ } else {
366
+ if (compatibility.requiredSuites.length > MAX_COMPATIBILITY_REQUIRED_SUITES) {
367
+ issues.push({
368
+ path: ["compatibility", "requiredSuites"],
369
+ message: `requiredSuites must contain at most ${MAX_COMPATIBILITY_REQUIRED_SUITES} entries`,
370
+ });
371
+ }
372
+ const seen = new Set<string>();
373
+ compatibility.requiredSuites.forEach((rawSuite, index) => {
374
+ if (!rawSuite || typeof rawSuite !== "object" || Array.isArray(rawSuite)) {
375
+ issues.push({ path: ["compatibility", "requiredSuites", index], message: "suite entry must be an object" });
376
+ return;
377
+ }
378
+ const suite = rawSuite as Record<string, unknown>;
379
+ const suiteId = typeof suite.suiteId === "string" ? suite.suiteId.trim() : "";
380
+ const layer = suite.evidenceLayer;
381
+ if (!suiteId) {
382
+ issues.push({ path: ["compatibility", "requiredSuites", index, "suiteId"], message: "suiteId is required" });
383
+ }
384
+ if (layer !== "protocol_conformance" && layer !== "live_route_compatibility") {
385
+ issues.push({
386
+ path: ["compatibility", "requiredSuites", index, "evidenceLayer"],
387
+ message: "evidenceLayer must be protocol_conformance or live_route_compatibility",
388
+ });
389
+ }
390
+ if (suiteId && typeof layer === "string" && EVIDENCE_LAYERS.has(layer)) {
391
+ const key = `${layer}:${suiteId}`;
392
+ if (seen.has(key)) {
393
+ issues.push({ path: ["compatibility", "requiredSuites", index], message: `duplicate required suite "${key}"` });
394
+ } else {
395
+ seen.add(key);
396
+ }
397
+ }
398
+ });
399
+ }
400
+ }
401
+ if (compatibility.minStatus !== undefined
402
+ && compatibility.minStatus !== "PROBED"
403
+ && compatibility.minStatus !== "VERIFIED") {
404
+ issues.push({ path: ["compatibility", "minStatus"], message: 'minStatus must be "PROBED" or "VERIFIED"' });
405
+ }
406
+ if (compatibility.maxEvidenceAgeMs !== undefined
407
+ && (typeof compatibility.maxEvidenceAgeMs !== "number"
408
+ || !Number.isInteger(compatibility.maxEvidenceAgeMs)
409
+ || compatibility.maxEvidenceAgeMs < 0)) {
410
+ issues.push({ path: ["compatibility", "maxEvidenceAgeMs"], message: "maxEvidenceAgeMs must be a non-negative integer" });
411
+ }
412
+ for (const key of ["unknownEvidence", "degradedEvidence"] as const) {
413
+ if (compatibility[key] !== undefined
414
+ && compatibility[key] !== "allow"
415
+ && compatibility[key] !== "penalize"
416
+ && compatibility[key] !== "exclude") {
417
+ issues.push({ path: ["compatibility", key], message: `${key} must be "allow", "penalize", or "exclude"` });
418
+ }
419
+ }
420
+ }
421
+ }
422
+
345
423
  return issues;
346
424
  }
347
425
 
@@ -372,6 +450,42 @@ function normalizedUnknownEvidence(raw: OcxRoutingProfileConfig): NormalizedRout
372
450
  return out;
373
451
  }
374
452
 
453
+ function normalizedCompatibility(
454
+ raw: OcxRoutingProfileConfig,
455
+ ): NormalizedRoutingProfileCompatibility | undefined {
456
+ if (raw.compatibility === undefined) return undefined;
457
+ const compatibility = raw.compatibility;
458
+ const requiredSuites = (compatibility.requiredSuites ?? []).map(suite => ({
459
+ suiteId: suite.suiteId.trim(),
460
+ evidenceLayer: suite.evidenceLayer,
461
+ }));
462
+ const unknownEvidence = compatibility.unknownEvidence === "allow"
463
+ || compatibility.unknownEvidence === "penalize"
464
+ || compatibility.unknownEvidence === "exclude"
465
+ ? compatibility.unknownEvidence
466
+ : DEFAULT_COMPATIBILITY_UNKNOWN_EVIDENCE;
467
+ const degradedEvidence = compatibility.degradedEvidence === "allow"
468
+ || compatibility.degradedEvidence === "penalize"
469
+ || compatibility.degradedEvidence === "exclude"
470
+ ? compatibility.degradedEvidence
471
+ : DEFAULT_DEGRADED_EVIDENCE;
472
+
473
+ const hasControls = requiredSuites.length > 0
474
+ || compatibility.minStatus !== undefined
475
+ || compatibility.maxEvidenceAgeMs !== undefined
476
+ || compatibility.unknownEvidence !== undefined
477
+ || compatibility.degradedEvidence !== undefined;
478
+ if (!hasControls) return undefined;
479
+
480
+ return {
481
+ requiredSuites,
482
+ ...(compatibility.minStatus ? { minStatus: compatibility.minStatus } : {}),
483
+ ...(compatibility.maxEvidenceAgeMs !== undefined ? { maxEvidenceAgeMs: compatibility.maxEvidenceAgeMs } : {}),
484
+ unknownEvidence,
485
+ degradedEvidence,
486
+ };
487
+ }
488
+
375
489
  function canonicalJson(value: unknown): string {
376
490
  if (value === null || typeof value !== "object") return JSON.stringify(value);
377
491
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -390,6 +504,7 @@ export function normalizeRoutingProfile(id: string, raw: OcxRoutingProfileConfig
390
504
  const weights = { ...DEFAULT_PROFILE_WEIGHTS, ...(raw.optimize ?? {}) };
391
505
  const weightSum = weights.latency + weights.health + weights.cost + weights.quota;
392
506
  const safeSum = weightSum > 0 ? weightSum : 1;
507
+ const compatibility = normalizedCompatibility(raw);
393
508
  const profile: Omit<NormalizedRoutingProfile, "revision"> = {
394
509
  id,
395
510
  alias: alias || null,
@@ -413,6 +528,7 @@ export function normalizeRoutingProfile(id: string, raw: OcxRoutingProfileConfig
413
528
  : {}),
414
529
  },
415
530
  unknownEvidence: normalizedUnknownEvidence(raw),
531
+ ...(compatibility ? { compatibility } : {}),
416
532
  };
417
533
  return { ...profile, revision: profileRevision(profile) };
418
534
  }
@@ -427,7 +543,5 @@ export function getRoutingProfile(
427
543
  }
428
544
 
429
545
  export function listRoutingProfileIds(config: { routingProfiles?: Record<string, OcxRoutingProfileConfig> }): string[] {
430
- // Code-unit comparison: deterministic across ICU versions/platforms, which
431
- // matters for the API/CLI list contract (ids may contain ".", "_", "-").
432
546
  return Object.keys(config.routingProfiles ?? {}).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
433
547
  }
@@ -102,6 +102,27 @@ export interface RouteCostEvidence {
102
102
  capOutcome?: RouteCostCapOutcome;
103
103
  }
104
104
 
105
+ export interface RouteCompatibilitySuiteTrace {
106
+ suiteId: string;
107
+ evidenceLayer: string;
108
+ /** Privacy-safe prefix of the exact subject used for this layer/suite. */
109
+ subjectIdPrefix?: string;
110
+ verdict?: string;
111
+ minStatus?: string;
112
+ fresh?: boolean;
113
+ unknownPolicy?: string;
114
+ degradedPolicy?: string;
115
+ outcome: "satisfied" | "penalized" | "excluded" | "unknown";
116
+ reason?: string;
117
+ }
118
+
119
+ export interface RouteCompatibilityEvidence {
120
+ /** Legacy single-subject prefix retained for persisted trace compatibility. */
121
+ subjectIdPrefix?: string;
122
+ suites: RouteCompatibilitySuiteTrace[];
123
+ truncated?: true;
124
+ }
125
+
105
126
  export interface RouteScoreEvidence {
106
127
  total: number;
107
128
  components: {
@@ -111,6 +132,7 @@ export interface RouteScoreEvidence {
111
132
  cost?: number;
112
133
  latency?: number;
113
134
  configuredPriority?: number;
135
+ compatibility?: number;
114
136
  };
115
137
  }
116
138
 
@@ -124,6 +146,7 @@ export interface RouteCandidateTrace {
124
146
  health?: RouteHealthEvidence;
125
147
  quota?: RouteQuotaEvidence;
126
148
  cost?: RouteCostEvidence;
149
+ compatibility?: RouteCompatibilityEvidence;
127
150
  score?: RouteScoreEvidence;
128
151
  }
129
152
 
@@ -149,6 +172,7 @@ export interface RouteDecisionTraceV1 {
149
172
  exclusions?: true;
150
173
  requirements?: true;
151
174
  strings?: true;
175
+ compatibility?: true;
152
176
  };
153
177
  }
154
178
 
@@ -202,6 +226,7 @@ export interface TraceCandidateInput {
202
226
  health?: RouteHealthEvidence;
203
227
  quota?: RouteQuotaEvidence;
204
228
  cost?: RouteCostEvidence;
229
+ compatibility?: RouteCompatibilityEvidence;
205
230
  }
206
231
 
207
232
  export interface TraceBuildInput {
@@ -222,18 +247,23 @@ export interface TraceBuildInput {
222
247
  now?: number;
223
248
  }
224
249
 
250
+ interface ParseCaps {
251
+ candidates?: true;
252
+ exclusions?: true;
253
+ requirements?: true;
254
+ strings?: true;
255
+ compatibility?: true;
256
+ }
257
+
225
258
  /** Bounded candidate copy: strings capped, exclusions sliced, score/evidence kept. */
226
- function buildCandidate(input: TraceCandidateInput, budget: { strings?: true; exclusions?: true }): RouteCandidateTrace {
259
+ function buildCandidate(input: TraceCandidateInput, budget: ParseCaps): RouteCandidateTrace {
227
260
  const exclusions = input.exclusions.slice(0, MAX_EXCLUSIONS_PER_CANDIDATE);
228
261
  if (exclusions.length < input.exclusions.length) budget.exclusions = true;
229
- // Evidence reaches the builder from internal producers (bounded) or from
230
- // caller-supplied dry-run input (unbounded). Whitelist + bound it through
231
- // the same parsers the persisted-row normalizer uses so no unknown nested
232
- // field or oversized string survives into the trace.
233
262
  const capability = input.capability ? parseCapability(input.capability, budget) : undefined;
234
263
  const health = input.health ? parseHealth(input.health) : undefined;
235
264
  const quota = input.quota ? parseQuota(input.quota, budget) : undefined;
236
265
  const cost = input.cost ? parseCost(input.cost, budget) : undefined;
266
+ const compatibility = input.compatibility ? parseCompatibility(input.compatibility, budget) : undefined;
237
267
  return {
238
268
  provider: capString(input.provider, budget),
239
269
  model: capString(input.model, budget),
@@ -252,6 +282,7 @@ function buildCandidate(input: TraceCandidateInput, budget: { strings?: true; ex
252
282
  ...(health ? { health } : {}),
253
283
  ...(quota ? { quota } : {}),
254
284
  ...(cost ? { cost } : {}),
285
+ ...(compatibility ? { compatibility } : {}),
255
286
  };
256
287
  }
257
288
 
@@ -278,7 +309,7 @@ function buildRequirement(requirement: RouteRequirementEvidence, budget: { strin
278
309
  * pass provider/model NAME strings and opaque account references only.
279
310
  */
280
311
  export function buildRouteDecisionTrace(input: TraceBuildInput): RouteDecisionTraceV1 {
281
- const budget: { strings?: true; exclusions?: true; candidates?: true } = {};
312
+ const budget: ParseCaps = {};
282
313
  const now = input.now ?? Date.now();
283
314
  const truncated: RouteDecisionTraceV1["truncated"] = {};
284
315
  let selectedIndex = Number.isInteger(input.selected.candidateIndex ?? 0)
@@ -287,8 +318,6 @@ export function buildRouteDecisionTrace(input: TraceBuildInput): RouteDecisionTr
287
318
 
288
319
  let candidates = (input.candidates ?? []).map(candidate => buildCandidate(candidate, budget));
289
320
  if (candidates.length > MAX_TRACE_CANDIDATES) {
290
- // Keep the selected candidate even when it sits beyond the slice: a trace
291
- // whose selected candidate vanished would contradict the decision.
292
321
  candidates = selectedIndex < MAX_TRACE_CANDIDATES
293
322
  ? candidates.slice(0, MAX_TRACE_CANDIDATES)
294
323
  : [...candidates.slice(0, MAX_TRACE_CANDIDATES - 1), candidates[selectedIndex]!];
@@ -296,7 +325,6 @@ export function buildRouteDecisionTrace(input: TraceBuildInput): RouteDecisionTr
296
325
  truncated.candidates = true;
297
326
  }
298
327
  if (candidates.length === 0) {
299
- // Invariant: every decision names at least the selected route as a candidate.
300
328
  candidates = [{
301
329
  provider: capString(input.selected.provider, budget),
302
330
  model: capString(input.selected.model, budget),
@@ -349,6 +377,7 @@ export function buildRouteDecisionTrace(input: TraceBuildInput): RouteDecisionTr
349
377
  if (budget.strings) truncated.strings = true;
350
378
  if (budget.exclusions) truncated.exclusions = true;
351
379
  if (budget.candidates) truncated.candidates = true;
380
+ if (budget.compatibility) truncated.compatibility = true;
352
381
  if (Object.keys(truncated).length > 0) trace.truncated = truncated;
353
382
 
354
383
  return enforceByteBudget(trace);
@@ -369,9 +398,6 @@ function enforceByteBudget(trace: RouteDecisionTraceV1): RouteDecisionTraceV1 {
369
398
  }));
370
399
  const slimmed: RouteDecisionTraceV1 = { ...trace, truncated, candidates };
371
400
  if (serializedByteLength(slimmed) <= MAX_TRACE_BYTES) return slimmed;
372
- // Second stage: shrink candidates. The selected candidate must survive and
373
- // `selected.candidateIndex` must keep pointing at it (same invariant as the
374
- // candidate-cap branch above).
375
401
  const half = Math.max(1, Math.floor(MAX_TRACE_CANDIDATES / 2));
376
402
  const selectedIndex = trace.selected.candidateIndex;
377
403
  const kept = selectedIndex < half
@@ -383,10 +409,6 @@ function enforceByteBudget(trace: RouteDecisionTraceV1): RouteDecisionTraceV1 {
383
409
  candidates: kept,
384
410
  selected: { ...slimmed.selected, candidateIndex: Math.min(selectedIndex, kept.length - 1) },
385
411
  };
386
- // Last resort: keep shrinking deterministically until the byte budget holds.
387
- // Each stage reduces a bounded dimension, and a single candidate with no
388
- // exclusions is provably below MAX_TRACE_BYTES given the string cap, so the
389
- // loop terminates.
390
412
  for (let stage = 0; stage < 4 && serializedByteLength(result) > MAX_TRACE_BYTES; stage++) {
391
413
  if (stage === 0) {
392
414
  result = {
@@ -420,16 +442,6 @@ function enforceByteBudget(trace: RouteDecisionTraceV1): RouteDecisionTraceV1 {
420
442
  return result;
421
443
  }
422
444
 
423
- // ---- defensive parsing of persisted rows --------------------------------------
424
-
425
- /** Caps applied by the normalizer; unioned into `truncated` on the result. */
426
- interface ParseCaps {
427
- candidates?: true;
428
- exclusions?: true;
429
- requirements?: true;
430
- strings?: true;
431
- }
432
-
433
445
  /** Defensive parse of one persisted exclusion reason. */
434
446
  function parseExclusion(raw: unknown, caps: ParseCaps): RouteExclusionReason | null {
435
447
  if (!isPlainRecord(raw)) return null;
@@ -571,13 +583,52 @@ function parseCost(raw: unknown, caps: ParseCaps): RouteCostEvidence | undefined
571
583
  return Object.keys(out).length > 0 ? out : undefined;
572
584
  }
573
585
 
586
+ const MAX_COMPATIBILITY_SUITES = 8;
587
+ const COMPATIBILITY_OUTCOMES = new Set(["satisfied", "penalized", "excluded", "unknown"]);
588
+
589
+ function parseCompatibility(raw: unknown, caps: ParseCaps): RouteCompatibilityEvidence | undefined {
590
+ if (!isPlainRecord(raw)) return undefined;
591
+ const suitesRaw = Array.isArray(raw.suites) ? raw.suites : [];
592
+ if (suitesRaw.length > MAX_COMPATIBILITY_SUITES || raw.truncated === true) caps.compatibility = true;
593
+ const suites: RouteCompatibilitySuiteTrace[] = [];
594
+ for (const entry of suitesRaw.slice(0, MAX_COMPATIBILITY_SUITES)) {
595
+ if (!isPlainRecord(entry)) continue;
596
+ const suiteId = entry.suiteId;
597
+ const evidenceLayer = entry.evidenceLayer;
598
+ const outcome = entry.outcome;
599
+ if (typeof suiteId !== "string" || typeof evidenceLayer !== "string") continue;
600
+ if (typeof outcome !== "string" || !COMPATIBILITY_OUTCOMES.has(outcome)) continue;
601
+ const row: RouteCompatibilitySuiteTrace = {
602
+ suiteId: capString(suiteId, caps),
603
+ evidenceLayer: capString(evidenceLayer, caps),
604
+ outcome: outcome as RouteCompatibilitySuiteTrace["outcome"],
605
+ };
606
+ if (typeof entry.subjectIdPrefix === "string" && entry.subjectIdPrefix) {
607
+ row.subjectIdPrefix = capString(entry.subjectIdPrefix, caps);
608
+ }
609
+ for (const key of ["verdict", "minStatus", "reason", "unknownPolicy", "degradedPolicy"] as const) {
610
+ if (typeof entry[key] === "string") row[key] = capString(entry[key] as string, caps);
611
+ }
612
+ if (typeof entry.fresh === "boolean") row.fresh = entry.fresh;
613
+ suites.push(row);
614
+ }
615
+ const out: RouteCompatibilityEvidence = {
616
+ suites,
617
+ ...(raw.truncated === true ? { truncated: true as const } : {}),
618
+ };
619
+ if (typeof raw.subjectIdPrefix === "string" && raw.subjectIdPrefix) {
620
+ out.subjectIdPrefix = capString(raw.subjectIdPrefix, caps);
621
+ }
622
+ return suites.length > 0 || out.subjectIdPrefix || out.truncated ? out : undefined;
623
+ }
624
+
574
625
  /** Whitelisted score parse; requires a finite `total` and bounded components. */
575
626
  function parseScore(raw: unknown): RouteScoreEvidence | undefined {
576
627
  if (!isPlainRecord(raw)) return undefined;
577
628
  if (!finiteNumber(raw.total)) return undefined;
578
629
  const components = isPlainRecord(raw.components) ? raw.components : {};
579
630
  const parsedComponents: RouteScoreEvidence["components"] = {};
580
- for (const key of ["capability", "health", "quota", "cost", "latency", "configuredPriority"] as const) {
631
+ for (const key of ["capability", "health", "quota", "cost", "latency", "configuredPriority", "compatibility"] as const) {
581
632
  if (finiteNumber(components[key])) parsedComponents[key] = components[key];
582
633
  }
583
634
  return { total: raw.total, components: parsedComponents };
@@ -604,6 +655,7 @@ function parseCandidate(raw: unknown, caps: ParseCaps): RouteCandidateTrace | nu
604
655
  const health = parseHealth(raw.health);
605
656
  const quota = parseQuota(raw.quota, caps);
606
657
  const cost = parseCost(raw.cost, caps);
658
+ const compatibility = parseCompatibility(raw.compatibility, caps);
607
659
  const score = parseScore(raw.score);
608
660
  return {
609
661
  provider: provider.slice(0, MAX_TRACE_STRING),
@@ -617,6 +669,7 @@ function parseCandidate(raw: unknown, caps: ParseCaps): RouteCandidateTrace | nu
617
669
  ...(health ? { health } : {}),
618
670
  ...(quota ? { quota } : {}),
619
671
  ...(cost ? { cost } : {}),
672
+ ...(compatibility ? { compatibility } : {}),
620
673
  ...(score ? { score } : {}),
621
674
  };
622
675
  }
@@ -695,6 +748,7 @@ export function normalizeRouteDecisionTrace(raw: unknown): RouteDecisionTraceV1
695
748
  if (incoming.exclusions === true || caps.exclusions) truncated.exclusions = true;
696
749
  if (incoming.requirements === true || caps.requirements) truncated.requirements = true;
697
750
  if (incoming.strings === true || caps.strings) truncated.strings = true;
751
+ if (incoming.compatibility === true || caps.compatibility) truncated.compatibility = true;
698
752
 
699
753
  return {
700
754
  version: 1,
@@ -717,6 +771,6 @@ export function normalizeRouteDecisionTrace(raw: unknown): RouteDecisionTraceV1
717
771
  ? { tieBreak: selected.tieBreak.slice(0, MAX_TRACE_STRING) }
718
772
  : {}),
719
773
  },
720
- ...(truncated && Object.keys(truncated).length > 0 ? { truncated } : {}),
774
+ ...(Object.keys(truncated).length > 0 ? { truncated } : {}),
721
775
  };
722
776
  }
@@ -6,6 +6,7 @@ import {
6
6
  modelAdapterRecordConfigError,
7
7
  modelPreferHostedToolsConfigError,
8
8
  codexAutoStartEnabled,
9
+ nonBlankStringArrayConfigError,
9
10
  positiveIntegerConfigError,
10
11
  positiveIntegerRecordConfigError,
11
12
  providerBaseUrlConfigError,
@@ -509,6 +510,11 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
509
510
  if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
510
511
  const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
511
512
  if (maxOutputError) return `provider ${name} ${maxOutputError}`;
513
+ const structuredOutputOptOutError = nonBlankStringArrayConfigError(
514
+ raw.noStructuredOutputModels,
515
+ "noStructuredOutputModels",
516
+ );
517
+ if (structuredOutputOptOutError) return `provider ${name} ${structuredOutputOptOutError}`;
512
518
  const openRouterError = openRouterRoutingConfigError(typed);
513
519
  if (openRouterError) return `provider ${name} ${openRouterError}`;
514
520
  if (typed.authMode === "local") {
@@ -589,6 +595,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
589
595
  "noTemperatureModels",
590
596
  "noTopPModels",
591
597
  "noPenaltyModels",
598
+ "noStructuredOutputModels",
592
599
  "autoToolChoiceOnlyModels",
593
600
  "preserveReasoningContentModels",
594
601
  "requiresReasoningPlaceholderModels",
@@ -14,6 +14,10 @@ import {
14
14
  cancelQueuedStorageWorkerSpawns,
15
15
  drainStorageWorkers,
16
16
  } from "../storage/worker-lifecycle";
17
+ import {
18
+ acquireServerResourceOwner,
19
+ type ServerResourceOwnerLease,
20
+ } from "../lib/server-resource-ownership";
17
21
  import {
18
22
  startMemoryWatchdog,
19
23
  type MemoryWatchdog,
@@ -29,6 +33,7 @@ type ProcessLoops = {
29
33
  type LeaseOwner = {
30
34
  token: symbol;
31
35
  applyPolicy: PolicyApply;
36
+ resources: ServerResourceOwnerLease;
32
37
  };
33
38
 
34
39
  export type ServerBackgroundLifecycleLease = {
@@ -103,6 +108,7 @@ function removeOwner(owner: LeaseOwner): boolean {
103
108
 
104
109
  function releaseOwnerSynchronously(owner: LeaseOwner): "inactive" | "shared" | "last" {
105
110
  if (!removeOwner(owner)) return "inactive";
111
+ owner.resources.release();
106
112
  const nextOwner = owners.at(-1);
107
113
  if (nextOwner) {
108
114
  setLivePolicyOwner(nextOwner.applyPolicy);
@@ -117,7 +123,8 @@ function releaseOwnerSynchronously(owner: LeaseOwner): "inactive" | "shared" | "
117
123
  *
118
124
  * The first live server starts the loops. Later servers only add a reference and
119
125
  * become the current policy sink; out-of-order release restores the newest
120
- * remaining sink. The final release owns timer and Worker teardown.
126
+ * remaining sink. The final release owns timer and Worker teardown. Resources
127
+ * registered while this server starts are released with this exact lease.
121
128
  */
122
129
  export function acquireServerBackgroundLifecycle(
123
130
  applyPolicy: PolicyApply,
@@ -129,13 +136,19 @@ export function acquireServerBackgroundLifecycle(
129
136
  const owner: LeaseOwner = {
130
137
  token: Symbol("server-background-lifecycle"),
131
138
  applyPolicy,
139
+ resources: acquireServerResourceOwner(),
132
140
  };
133
- if (!processLoops) {
134
- processLoops = startProcessLoops(applyPolicy);
135
- } else {
136
- setLivePolicyOwner(applyPolicy);
141
+ try {
142
+ if (!processLoops) {
143
+ processLoops = startProcessLoops(applyPolicy);
144
+ } else {
145
+ setLivePolicyOwner(applyPolicy);
146
+ }
147
+ owners.push(owner);
148
+ } catch (error) {
149
+ owner.resources.release();
150
+ throw error;
137
151
  }
138
- owners.push(owner);
139
152
 
140
153
  let releaseFlight: Promise<void> | null = null;
141
154
  return {
@@ -166,4 +179,4 @@ export function acquireServerBackgroundLifecycle(
166
179
  releaseFlight = Promise.resolve();
167
180
  },
168
181
  };
169
- }
182
+ }
@@ -25,7 +25,7 @@ import type { OcxConfig } from "../types";
25
25
  import { readJsonRequestBody } from "./request-decompress";
26
26
  import {
27
27
  addFinalRequestLog,
28
- httpStatusForTerminalStatus,
28
+ httpStatusForRequestLogTerminal,
29
29
  recordFirstOutput,
30
30
  type RequestLogContext,
31
31
  type RequestLogEntry,
@@ -216,7 +216,7 @@ async function handleChatCompletionsWithBudget(
216
216
  inboundWire: "chat",
217
217
  translatorBudget,
218
218
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
219
- onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForTerminalStatus(status), { terminalStatus: status, closeReason: "terminal" }),
219
+ onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
220
220
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
221
221
  });
222
222
 
@@ -31,7 +31,7 @@ import { evidenceFromBody } from "../routing/request-evidence";
31
31
  import { resolveWireProtocolOverride } from "./adapter-resolve";
32
32
  import type { OcxConfig } from "../types";
33
33
  import { readJsonRequestBody } from "./request-decompress";
34
- import { addFinalRequestLog, httpStatusForTerminalStatus, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
34
+ import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
35
35
  import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
36
36
  import { responseWithDeferredRequestLog } from "./relay";
37
37
  import { handleResponses } from "./responses";
@@ -605,7 +605,10 @@ async function handleClaudeMessagesWithBudget(
605
605
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
606
606
  }
607
607
  if (isRec(anthropicBody) && effortOverride) {
608
- anthropicBody.output_config = { effort: effortOverride };
608
+ anthropicBody.output_config = {
609
+ ...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
610
+ effort: effortOverride,
611
+ };
609
612
  delete anthropicBody.thinking;
610
613
  }
611
614
  const translation = anthropicToResponsesTranslation(anthropicBody, config.claudeCode);
@@ -735,7 +738,7 @@ async function handleClaudeMessagesWithBudget(
735
738
  stripClaudeMainAuthForNoncanonicalForward: true,
736
739
  translatorBudget,
737
740
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
738
- onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForTerminalStatus(status), { terminalStatus: status, closeReason: "terminal" }),
741
+ onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
739
742
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
740
743
  });
741
744
  const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;