@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,640 @@
1
+ import * as z from "zod/v4";
2
+ import {
3
+ agentTaskRecoverySchema,
4
+ catalogAutoRefreshSchema,
5
+ clientConnectionSchema,
6
+ CODEX_ACCOUNT_PIN_PATTERN,
7
+ codexAccountPrioritiesSchema,
8
+ codexPoolSchema,
9
+ codexQuotaAutoRefreshSchema,
10
+ credentialGroupsSchema,
11
+ hubConfigSchema,
12
+ providerConfigSchema,
13
+ quotaResetNotifySchema,
14
+ remoteGuiConfigSchema,
15
+ runtimeRoleSchema,
16
+ configuredCodexPoolAccountIds,
17
+ apiKeyEntrySchema,
18
+ asideProfileSyncSchema,
19
+ clientIntegrationsSchema,
20
+ CODEX_ACCOUNT_NAMESPACE_ACCOUNT_ID_COLLISION_ERROR,
21
+ codexAccountNamespacesSchema,
22
+ modelPinnedEffortsSchema,
23
+ modelPreferHostedToolsConfigError,
24
+ providerModelCostsConfigError,
25
+ providerRelativeSendPathConfigError,
26
+ } from "./leaf-validators";
27
+ import { isValidProviderName, hasOwnProvider } from "../provider-name";
28
+ import {
29
+ apiKeyTransportConfigError,
30
+ booleanRecordConfigError,
31
+ modelAdapterRecordConfigError,
32
+ modelDisplayNamesConfigError,
33
+ nonBlankStringArrayConfigError,
34
+ positiveIntegerConfigError,
35
+ positiveIntegerRecordConfigError,
36
+ providerBaseUrlConfigError,
37
+ providerHeadersConfigError,
38
+ reasoningSummaryDeliveryRecordConfigError,
39
+ } from "../provider-validation";
40
+ import {
41
+ CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
42
+ codexAccountNamespaceForModel,
43
+ codexProviderNamespaceKey,
44
+ MAIN_CODEX_ACCOUNT_NAMESPACE_TARGET,
45
+ } from "../../codex/account-namespace-match";
46
+ import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../../codex/upstream-host-health";
47
+ import { COMBO_NAMESPACE, comboConfigIssues } from "../../combos/types";
48
+ import { routingProfileIssues } from "../../routing/profile";
49
+ import { POLICY_NAMESPACE } from "../../routing/profile-namespace";
50
+ import { providerDestinationConfigError } from "../../lib/destination-policy";
51
+ import { redactSecretString } from "../../lib/redact";
52
+ import { openRouterRoutingConfigError } from "../../providers/openrouter-routing";
53
+ import { vercelGatewayRoutingConfigError } from "../../providers/vercel-gateway-routing";
54
+ import { type OcxApiKeyEntry, type OcxProviderConfig } from "../../types";
55
+ import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
56
+ import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
57
+ import { hasFastWireCapabilityConflict } from "../../providers/fastwire";
58
+ import { parseDesktopProfile } from "../../claude/desktop-profile";
59
+ import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
60
+
61
+ export const configSchema = z.object({
62
+ port: z.number().int().min(0).max(65535).default(10100),
63
+ // A malformed hand edit must disable only remote-role behavior, not discard
64
+ // providers or data-plane keys. Live writes are rejected explicitly below.
65
+ runtimeRole: runtimeRoleSchema.optional().catch(undefined),
66
+ // Malformed optional remote blocks disable only remote GUI behavior. Live
67
+ // candidates are rejected explicitly by remoteGuiConfigError below.
68
+ hub: hubConfigSchema.optional().catch(undefined),
69
+ remoteGui: remoteGuiConfigSchema.optional().catch(undefined),
70
+ // A malformed privacy block must never be read as "unmask": .catch(undefined) drops it and
71
+ // emailMaskingEnabled then falls back to masked, which is also what an absent block means.
72
+ privacy: z.object({ maskEmails: z.boolean().optional() }).strict().optional().catch(undefined),
73
+ // A malformed present client block must remain diagnosable from raw config and
74
+ // fail closed through src/client/state.ts; unrelated provider state still loads.
75
+ client: clientConnectionSchema.optional().catch(undefined),
76
+ managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024).describe(
77
+ "Deprecated compatibility limit for bounded legacy usage readers; GET /api/usage always aggregates the complete ledger",
78
+ ),
79
+ // Invalid hand edits disable only this opt-in circuit. Live writes remain strict.
80
+ upstreamHostCircuitThreshold: z.number().int()
81
+ .min(0)
82
+ .max(UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD)
83
+ .optional()
84
+ .catch(undefined),
85
+ // Opt-in outbound body ceiling. An invalid hand edit disables only this guard, matching the
86
+ // circuit threshold above: a malformed number must not make the proxy refuse traffic.
87
+ maxUpstreamBodyBytes: z.number().int()
88
+ .min(0)
89
+ .optional()
90
+ .catch(undefined),
91
+ // Opt-in inbound body ceiling (#3573). An invalid hand edit degrades to the 256 MiB default
92
+ // rather than failing the parse, matching the outbound guard above: a malformed number must
93
+ // not change what the proxy admits. The hard ceiling is NOT enforced here — because of that
94
+ // `.catch`, and because a config object can be built without this schema at all — but in
95
+ // `resolveInboundBodyLimitBytes()`, which every reader goes through.
96
+ maxInboundBodyBytes: z.number().int()
97
+ .min(0)
98
+ .optional()
99
+ .catch(undefined),
100
+ appOwnedMemoryBudgetMb: z.number().int()
101
+ .min(MIN_APP_OWNED_MEMORY_BUDGET_MB)
102
+ .max(MAX_APP_OWNED_MEMORY_BUDGET_MB)
103
+ .default(DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024))
104
+ .catch(DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024)),
105
+ // A blank hostname degrades to undefined rather than failing the parse. `getDefaultConfig()`
106
+ // carries no `hostname` key, so the backup-and-defaults repair path below cannot merge one
107
+ // away — a hand-edited `"hostname": ""` would fail twice and reset providers/apiKeys to
108
+ // defaults, which is strictly worse than the bind bug this validation exists for. Degrading
109
+ // is safe: startServer() already falls back to 127.0.0.1 for a missing hostname. Write-time
110
+ // rejection lives in validateConfigCandidate() so bad values still surface to the caller.
111
+ hostname: z.string().trim().min(1).optional().catch(undefined),
112
+ // Discriminated on `enabled` so a disabled entry cannot be forced to carry a port (#1102).
113
+ // An enabled one MAY omit it: that is the companion form, which binds 127.0.0.1 on the proxy
114
+ // port and is legal only off a loopback/wildcard bind — a relationship between two fields, so
115
+ // it is enforced in validateConfigCandidate() and again at startup, not here (#4236).
116
+ // A malformed value degrades to undefined rather than failing the whole parse: this is an
117
+ // opt-in convenience surface, and a hand-edit typo here must never reset providers/apiKeys
118
+ // through the backup-and-defaults repair path.
119
+ unauthenticatedLoopbackListener: z.union([
120
+ z.object({ enabled: z.literal(false) }),
121
+ z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535).optional() }),
122
+ ]).optional().catch(undefined),
123
+ providers: z.record(z.string(), providerConfigSchema),
124
+ modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
125
+ defaultProvider: z.string().min(1).default("openai"),
126
+ defaultModelAliases: z.boolean().optional(),
127
+ // Malformed hand edits disable this opt-in projection without rejecting providers.
128
+ cursorEffortRows: z.boolean().optional().catch(false),
129
+ // Fast selectors default on; malformed hand edits disable them without rejecting providers.
130
+ fastRows: z.boolean().default(true).catch(false),
131
+ // Ultra Fast is opt-in for the same reason and degrades the same way: a malformed hand
132
+ // edit turns the tier off rather than rejecting the config that carries it.
133
+ ultraFastTier: z.boolean().optional().catch(false),
134
+ codexMainAccountHardLock: z.boolean().optional().catch(false),
135
+ // Future versions remain opaque through passthrough-compatible whole-config saves.
136
+ // Only version 1 grants deletion authority in the rebase path.
137
+ configRebaseProvenance: z.unknown().optional(),
138
+ // A retry can be billable, so absence and malformed hand edits both stay off.
139
+ emptyCompletionRetry: z.boolean().optional().catch(false),
140
+ // Header suppression changes what Codex sees, so absence and malformed edits stay off.
141
+ dropCodexSafetyBuffering: z.boolean().optional().catch(false),
142
+ // A malformed hand edit must not silently stop opening the browser: fall back
143
+ // to undefined, which resolves to the historical auto-open behavior.
144
+ oauthOpenBrowser: z.boolean().optional().catch(undefined),
145
+ openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
146
+ // Invalid hand edits must not discard an otherwise usable config.
147
+ googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
148
+ subagentModelsVersion: z.number().int().positive().optional().catch(undefined),
149
+ subagentModels: z.array(z.string().min(1)).optional().catch(undefined),
150
+ // A hand-edited advisory version must not cost the operator their providers; a bad
151
+ // value degrades to undefined, which simply raises the notice again.
152
+ multiAgentSurfaceAdvisoryVersion: z.number().int().nonnegative().optional().catch(undefined),
153
+ clientIntegrations: clientIntegrationsSchema.optional().catch(undefined),
154
+ // A malformed profile policy must not fall back to legacy all-profile activation.
155
+ asideProfileSync: asideProfileSyncSchema.optional().catch({ allProfiles: false }),
156
+ providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
157
+ providerContextCapValues: z.record(z.string(), z.number().int().positive()).optional(),
158
+ contextCapValue: z.number().int().positive().optional(),
159
+ multiAgentGuidanceEnabled: z.boolean().optional(),
160
+ // Invalid optional recovery config must not discard unrelated provider/account state.
161
+ plaintextV2AgentMessages: z.boolean().optional().catch(undefined),
162
+ agentTaskRecovery: agentTaskRecoverySchema.optional().catch(undefined),
163
+ // Same rationale: a bad notify section must not cost the operator their providers.
164
+ quotaResetNotify: quotaResetNotifySchema.optional().catch(undefined),
165
+ // Same rationale: a bad auto-refresh section must not cost the operator their providers.
166
+ catalogAutoRefresh: catalogAutoRefreshSchema.optional().catch(undefined),
167
+ // These selections pre-date schema validation and used to pass through as
168
+ // unknown fields. Invalid hand edits must disable only the optional
169
+ // delegation/native-default feature, not reject the whole config and hide
170
+ // otherwise valid providers, accounts, or the configured listen port.
171
+ injectionModel: z.string().optional().catch(undefined),
172
+ injectionEffort: z.string().optional().catch(undefined),
173
+ syncCodexSubagentDefaults: z.boolean().optional().catch(undefined),
174
+ // Per-primary-model fallback chains. Values must be non-empty string arrays;
175
+ // malformed entries degrade to undefined rather than rejecting the whole config.
176
+ subagentModelFallbackByModel: z.record(
177
+ z.string(),
178
+ z.array(z.string().trim().min(1)).min(1),
179
+ ).optional().catch(undefined),
180
+ codexShimAutoRestore: z.boolean().optional(),
181
+ codexDesktopAuthless: z.boolean().optional().catch(undefined),
182
+ codexClientCompaction: z.boolean().optional().catch(undefined),
183
+ pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
184
+ // A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
185
+ // typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
186
+ // Silently ignoring it would be its own trap, so the write path rejects it and loadConfig warns.
187
+ codexPool: codexPoolSchema.optional().catch(undefined),
188
+ codexQuotaAutoRefresh: codexQuotaAutoRefreshSchema.optional().catch(undefined),
189
+ codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
190
+ // Selection order is a preference, not a safety control like pause: a malformed
191
+ // map degrades to "no ordering" rather than failing the parse, so a hand-edited
192
+ // typo cannot trip the backup-and-defaults repair path and wipe providers or
193
+ // pool accounts. Warning emitted in loadConfig.
194
+ codexAccountPriorities: codexAccountPrioritiesSchema.optional().catch(undefined),
195
+ activeCodexAccountPinned: z.string().regex(CODEX_ACCOUNT_PIN_PATTERN).optional().catch(undefined),
196
+ // A malformed hand edit must degrade to false without discarding providers, accounts,
197
+ // or the exact selector map. Live writes remain strict.
198
+ codexAccountPickerEnabled: z.boolean().optional().catch(false),
199
+ resetCreditAutoRedeem: z.object({
200
+ enabled: z.boolean().optional(),
201
+ leadTimeMinutes: z.number().int().min(1).max(60).optional(),
202
+ }).optional().catch(undefined),
203
+ // Same degrade-to-off rule as the flags above: a hand-edited typo in an opt-in pool
204
+ // feature must never cost the operator their providers.
205
+ pool: z.object({
206
+ kernel: z.boolean().optional(),
207
+ cacheAffinity: z.boolean().optional(),
208
+ // The catch belongs on the list, not on `pool`. Left to the outer catch below, one
209
+ // malformed group failed this nested object and dropped the whole `pool` -- taking
210
+ // `kernel` and `cacheAffinity` with it, which is a live routing change the operator
211
+ // never made. Scoped here, a malformed or ambiguous group costs only the declared
212
+ // grouping: loadConfig warns, and the write path rejects it outright.
213
+ credentialGroups: credentialGroupsSchema.optional().catch(undefined),
214
+ }).optional().catch(undefined),
215
+ // Model ids excluded from the Grok Build managed block (dashboard switches).
216
+ grokExcludedModels: z.array(z.string()).optional(),
217
+ // Invalid values degrade to undefined ("auto") instead of failing the whole
218
+ // parse: a hand-edited typo must never trip the backup-and-defaults repair
219
+ // path below and wipe providers/pool accounts. Warning emitted in loadConfig.
220
+ streamMode: z.enum(["auto", "legacy-tee", "eager-relay"]).optional().catch(undefined),
221
+ blockedModelRedirects: z.record(z.string(), z.string()).optional().catch(undefined),
222
+ // Same degrade-don't-reject rationale as the fields above: a hand-edited
223
+ // non-string must not trip the backup-and-defaults repair path. Unset then
224
+ // takes the canonical sideband path (src/server/live.ts normalizeSidebandRoot).
225
+ experimentalRealtimeWsBaseUrl: z.string().optional().catch(undefined),
226
+ // Salvage element by element, and never fail the parse. Two spellings were
227
+ // measured on this zod version and both lose data:
228
+ // `z.array(entry).catch(undefined)` -> one bad entry discards EVERY key
229
+ // `z.array(z.unknown())` -> a non-array value still raises
230
+ // invalid_type, reaching the
231
+ // backup-and-defaults repair path
232
+ // Starting from `unknown` is what makes both survivable. A key the user still
233
+ // has deployed must not be collateral damage for one bad neighbour, and on a
234
+ // remote bind an emptied array is worse than cosmetic: assertServerAuthConfig
235
+ // refuses to start without a data credential.
236
+ apiKeys: z.unknown().optional().transform(value => {
237
+ if (value === undefined) return undefined;
238
+ if (!Array.isArray(value)) return undefined;
239
+ return value
240
+ .filter(row => apiKeyEntrySchema.safeParse(row).success)
241
+ .map(row => apiKeyEntrySchema.parse(row) as OcxApiKeyEntry);
242
+ }),
243
+ }).passthrough().superRefine((config, ctx) => {
244
+ const claudeCode = (config as { claudeCode?: unknown }).claudeCode;
245
+ if (claudeCode !== undefined && (!claudeCode || typeof claudeCode !== "object" || Array.isArray(claudeCode))) {
246
+ ctx.addIssue({ code: "custom", path: ["claudeCode"], message: "claudeCode must be an object" });
247
+ } else if (claudeCode) {
248
+ const claude = claudeCode as { desktopProfile?: unknown };
249
+ if (claude.desktopProfile !== undefined) {
250
+ try {
251
+ parseDesktopProfile(claude.desktopProfile);
252
+ } catch (error) {
253
+ ctx.addIssue({
254
+ code: "custom",
255
+ path: ["claudeCode", "desktopProfile"],
256
+ message: error instanceof Error ? error.message : String(error),
257
+ });
258
+ }
259
+ }
260
+ }
261
+
262
+ const accountNamespaces = config.codexAccountNamespaces;
263
+ if (accountNamespaces) {
264
+ const configuredAccountIds = configuredCodexPoolAccountIds(config.codexAccounts);
265
+ const configuredProviderNamespaces = new Set([
266
+ COMBO_NAMESPACE,
267
+ OPENAI_CODEX_PROVIDER_ID,
268
+ POLICY_NAMESPACE,
269
+ ...Object.keys(config.providers),
270
+ ].map(codexProviderNamespaceKey));
271
+ const namespaceTargets = new Set(
272
+ Object.values(accountNamespaces)
273
+ .filter(accountId => accountId !== MAIN_CODEX_ACCOUNT_NAMESPACE_TARGET),
274
+ );
275
+ for (const namespace of Object.keys(accountNamespaces)) {
276
+ if (configuredProviderNamespaces.has(codexProviderNamespaceKey(namespace))) {
277
+ ctx.addIssue({
278
+ code: "custom",
279
+ path: ["codexAccountNamespaces", namespace],
280
+ message: "account selectors must not collide with configured provider, combo, or routing policy namespaces",
281
+ });
282
+ }
283
+ if (configuredAccountIds.has(namespace) || namespaceTargets.has(namespace)) {
284
+ ctx.addIssue({
285
+ code: "custom",
286
+ path: ["codexAccountNamespaces", namespace],
287
+ message: CODEX_ACCOUNT_NAMESPACE_ACCOUNT_ID_COLLISION_ERROR,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ for (const name of Object.keys(config.providers)) {
293
+ if (!isValidProviderName(name)) {
294
+ ctx.addIssue({
295
+ code: "custom",
296
+ path: ["providers", redactSecretString(name)],
297
+ message: "provider names must use letters, numbers, dot, underscore, or hyphen and cannot be reserved JavaScript object keys or routing namespaces (policy)",
298
+ });
299
+ }
300
+ const provider = config.providers[name];
301
+ if (hasFastWireCapabilityConflict(provider)) {
302
+ ctx.addIssue({
303
+ code: "custom",
304
+ path: ["providers", redactSecretString(name), "fastWire"],
305
+ message: "fastWire=null conflicts with supportsServiceTier=true",
306
+ });
307
+ }
308
+ const openRouterRoutingError = openRouterRoutingConfigError(provider);
309
+ if (openRouterRoutingError) {
310
+ ctx.addIssue({
311
+ code: "custom",
312
+ path: [
313
+ "providers",
314
+ redactSecretString(name),
315
+ openRouterRoutingError.startsWith("modelOpenRouterRouting")
316
+ ? "modelOpenRouterRouting"
317
+ : "openRouterRouting",
318
+ ],
319
+ message: openRouterRoutingError,
320
+ });
321
+ }
322
+ const vercelRoutingError = vercelGatewayRoutingConfigError(provider);
323
+ if (vercelRoutingError) {
324
+ ctx.addIssue({
325
+ code: "custom",
326
+ path: [
327
+ "providers",
328
+ redactSecretString(name),
329
+ vercelRoutingError.startsWith("modelVercelGatewayRouting")
330
+ ? "modelVercelGatewayRouting"
331
+ : "vercelGatewayRouting",
332
+ ],
333
+ message: vercelRoutingError,
334
+ });
335
+ }
336
+ if (Object.hasOwn(provider, "virtualModels")) {
337
+ ctx.addIssue({
338
+ code: "custom",
339
+ path: ["providers", redactSecretString(name), "virtualModels"],
340
+ message: "virtualModels is registry-only and must not be persisted",
341
+ });
342
+ }
343
+ const baseUrlError = providerBaseUrlConfigError(provider.baseUrl);
344
+ if (baseUrlError) {
345
+ ctx.addIssue({
346
+ code: "custom",
347
+ path: ["providers", redactSecretString(name), "baseUrl"],
348
+ message: baseUrlError,
349
+ });
350
+ } else {
351
+ const destinationError = providerDestinationConfigError(name, provider);
352
+ if (destinationError) {
353
+ ctx.addIssue({
354
+ code: "custom",
355
+ path: ["providers", redactSecretString(name), "baseUrl"],
356
+ message: destinationError,
357
+ });
358
+ }
359
+ }
360
+ for (const field of ["responsesPath", "chatCompletionsPath"] as const) {
361
+ const sendPathError = providerRelativeSendPathConfigError(field, provider[field]);
362
+ if (sendPathError) {
363
+ ctx.addIssue({
364
+ code: "custom",
365
+ path: ["providers", redactSecretString(name), field],
366
+ message: sendPathError,
367
+ });
368
+ }
369
+ }
370
+ const headersError = providerHeadersConfigError((provider as { headers?: unknown }).headers);
371
+ if (headersError) {
372
+ ctx.addIssue({
373
+ code: "custom",
374
+ path: ["providers", redactSecretString(name), "headers"],
375
+ message: headersError,
376
+ });
377
+ }
378
+ const modelCostsError = providerModelCostsConfigError((provider as { modelCosts?: unknown }).modelCosts);
379
+ if (modelCostsError) {
380
+ ctx.addIssue({
381
+ code: "custom",
382
+ // The provider key is caller-controlled and can be token-shaped; redact it
383
+ // before schemaDiagnosticsError serializes the path (ocx config validate/import).
384
+ path: ["providers", redactSecretString(name), "modelCosts"],
385
+ message: modelCostsError,
386
+ });
387
+ }
388
+ const modelDisplayNamesError = modelDisplayNamesConfigError(
389
+ (provider as { modelDisplayNames?: unknown }).modelDisplayNames,
390
+ );
391
+ if (modelDisplayNamesError) {
392
+ ctx.addIssue({
393
+ code: "custom",
394
+ path: ["providers", redactSecretString(name), "modelDisplayNames"],
395
+ message: modelDisplayNamesError,
396
+ });
397
+ }
398
+ const apiKeyTransportError = apiKeyTransportConfigError(provider as OcxProviderConfig);
399
+ if (apiKeyTransportError) {
400
+ ctx.addIssue({
401
+ code: "custom",
402
+ path: ["providers", redactSecretString(name), "apiKeyTransport"],
403
+ message: apiKeyTransportError,
404
+ });
405
+ }
406
+ const modelAdaptersError = modelAdapterRecordConfigError(
407
+ (provider as { modelAdapters?: unknown }).modelAdapters,
408
+ "modelAdapters",
409
+ name,
410
+ provider,
411
+ );
412
+ if (modelAdaptersError) {
413
+ ctx.addIssue({
414
+ code: "custom",
415
+ path: ["providers", redactSecretString(name), "modelAdapters"],
416
+ message: modelAdaptersError,
417
+ });
418
+ }
419
+ const preferHostedToolsError = modelPreferHostedToolsConfigError(
420
+ (provider as { modelPreferHostedTools?: unknown }).modelPreferHostedTools,
421
+ "modelPreferHostedTools",
422
+ name,
423
+ provider,
424
+ );
425
+ if (preferHostedToolsError) {
426
+ ctx.addIssue({
427
+ code: "custom",
428
+ path: ["providers", redactSecretString(name), "modelPreferHostedTools"],
429
+ message: preferHostedToolsError,
430
+ });
431
+ }
432
+ const maxInputError = positiveIntegerRecordConfigError(
433
+ (provider as { modelMaxInputTokens?: unknown }).modelMaxInputTokens,
434
+ "modelMaxInputTokens",
435
+ );
436
+ if (maxInputError) {
437
+ ctx.addIssue({
438
+ code: "custom",
439
+ path: ["providers", redactSecretString(name), "modelMaxInputTokens"],
440
+ message: maxInputError,
441
+ });
442
+ }
443
+ const autoCompactError = modelAutoCompactTokenLimitsConfigError(
444
+ (provider as { modelAutoCompactTokenLimits?: unknown }).modelAutoCompactTokenLimits,
445
+ { requireNativeIds: name === OPENAI_CODEX_PROVIDER_ID },
446
+ );
447
+ if (autoCompactError) {
448
+ ctx.addIssue({
449
+ code: "custom",
450
+ path: ["providers", redactSecretString(name), "modelAutoCompactTokenLimits"],
451
+ message: autoCompactError,
452
+ });
453
+ }
454
+ const reasoningSummariesError = booleanRecordConfigError(
455
+ (provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
456
+ "modelSupportsReasoningSummaries",
457
+ );
458
+ if (reasoningSummariesError) {
459
+ ctx.addIssue({
460
+ code: "custom",
461
+ path: ["providers", redactSecretString(name), "modelSupportsReasoningSummaries"],
462
+ message: reasoningSummariesError,
463
+ });
464
+ }
465
+ const verbositySupportError = booleanRecordConfigError(
466
+ (provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
467
+ "modelSupportsVerbosity",
468
+ );
469
+ if (verbositySupportError) {
470
+ ctx.addIssue({
471
+ code: "custom",
472
+ path: ["providers", redactSecretString(name), "modelSupportsVerbosity"],
473
+ message: verbositySupportError,
474
+ });
475
+ }
476
+ const serviceTierModelsError = booleanRecordConfigError(
477
+ (provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
478
+ "modelSupportsServiceTier",
479
+ );
480
+ if (serviceTierModelsError) {
481
+ ctx.addIssue({
482
+ code: "custom",
483
+ path: ["providers", redactSecretString(name), "modelSupportsServiceTier"],
484
+ message: serviceTierModelsError,
485
+ });
486
+ }
487
+ const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
488
+ (provider as { modelReasoningSummaryDelivery?: unknown }).modelReasoningSummaryDelivery,
489
+ (provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
490
+ );
491
+ if (reasoningSummaryDeliveryError) {
492
+ ctx.addIssue({
493
+ code: "custom",
494
+ path: ["providers", redactSecretString(name), "modelReasoningSummaryDelivery"],
495
+ message: reasoningSummaryDeliveryError,
496
+ });
497
+ }
498
+ const defaultMaxOutputError = positiveIntegerConfigError(
499
+ (provider as { defaultMaxOutputTokens?: unknown }).defaultMaxOutputTokens,
500
+ "defaultMaxOutputTokens",
501
+ );
502
+ if (defaultMaxOutputError) {
503
+ ctx.addIssue({
504
+ code: "custom",
505
+ path: ["providers", redactSecretString(name), "defaultMaxOutputTokens"],
506
+ message: defaultMaxOutputError,
507
+ });
508
+ }
509
+ const maxOutputError = positiveIntegerRecordConfigError(
510
+ (provider as { modelMaxOutputTokens?: unknown }).modelMaxOutputTokens,
511
+ "modelMaxOutputTokens",
512
+ );
513
+ if (maxOutputError) {
514
+ ctx.addIssue({
515
+ code: "custom",
516
+ path: ["providers", redactSecretString(name), "modelMaxOutputTokens"],
517
+ message: maxOutputError,
518
+ });
519
+ }
520
+ const structuredOutputOptOutError = nonBlankStringArrayConfigError(
521
+ (provider as { noStructuredOutputModels?: unknown }).noStructuredOutputModels,
522
+ "noStructuredOutputModels",
523
+ );
524
+ if (structuredOutputOptOutError) {
525
+ ctx.addIssue({
526
+ code: "custom",
527
+ path: ["providers", redactSecretString(name), "noStructuredOutputModels"],
528
+ message: structuredOutputOptOutError,
529
+ });
530
+ }
531
+ const jsonSchemaOptOutError = nonBlankStringArrayConfigError(
532
+ (provider as { noJsonSchemaModels?: unknown }).noJsonSchemaModels,
533
+ "noJsonSchemaModels",
534
+ );
535
+ if (jsonSchemaOptOutError) {
536
+ ctx.addIssue({
537
+ code: "custom",
538
+ path: ["providers", redactSecretString(name), "noJsonSchemaModels"],
539
+ message: jsonSchemaOptOutError,
540
+ });
541
+ }
542
+ const retainModelsError = nonBlankStringArrayConfigError(
543
+ (provider as { retainModels?: unknown }).retainModels,
544
+ "retainModels",
545
+ );
546
+ if (retainModelsError) {
547
+ ctx.addIssue({
548
+ code: "custom",
549
+ path: ["providers", redactSecretString(name), "retainModels"],
550
+ message: retainModelsError,
551
+ });
552
+ }
553
+ const toolReasoningOptOutError = nonBlankStringArrayConfigError(
554
+ (provider as { omitReasoningEffortWithToolsModels?: unknown }).omitReasoningEffortWithToolsModels,
555
+ "omitReasoningEffortWithToolsModels",
556
+ );
557
+ if (toolReasoningOptOutError) {
558
+ ctx.addIssue({
559
+ code: "custom",
560
+ path: ["providers", redactSecretString(name), "omitReasoningEffortWithToolsModels"],
561
+ message: toolReasoningOptOutError,
562
+ });
563
+ }
564
+ if (Object.hasOwn(provider, "codexAccountMode") && provider.codexAccountMode !== undefined) {
565
+ // Persisted account mode is valid ONLY on the canonical built-in `openai` forward provider.
566
+ // Old openai-multi rows stay parseable (they never carry a mode) so startup can migrate them.
567
+ const canonicalOpenAiShape = name === "openai"
568
+ && provider.adapter === "openai-responses"
569
+ && (provider as { authMode?: unknown }).authMode === "forward"
570
+ && typeof provider.baseUrl === "string"
571
+ && provider.baseUrl.replace(/\/+$/, "") === "https://chatgpt.com/backend-api/codex";
572
+ if (!canonicalOpenAiShape) {
573
+ ctx.addIssue({
574
+ code: "custom",
575
+ path: ["providers", redactSecretString(name), "codexAccountMode"],
576
+ message: "codexAccountMode is valid only on the canonical built-in openai provider",
577
+ });
578
+ }
579
+ }
580
+ }
581
+ if (!hasOwnProvider(config.providers, config.defaultProvider)) {
582
+ ctx.addIssue({
583
+ code: "custom",
584
+ path: ["defaultProvider"],
585
+ message: "defaultProvider must exist in providers",
586
+ });
587
+ }
588
+ const combos = (config as { combos?: unknown }).combos;
589
+ if (combos !== undefined) {
590
+ if (!combos || typeof combos !== "object" || Array.isArray(combos)) {
591
+ ctx.addIssue({ code: "custom", path: ["combos"], message: "combos must be an object" });
592
+ } else {
593
+ for (const [id, raw] of Object.entries(combos as Record<string, unknown>)) {
594
+ const alias = raw && typeof raw === "object" && !Array.isArray(raw)
595
+ ? (raw as { alias?: unknown }).alias
596
+ : undefined;
597
+ if (typeof alias === "string" && codexAccountNamespaceForModel(accountNamespaces, alias.trim())) {
598
+ ctx.addIssue({
599
+ code: "custom",
600
+ path: ["combos", id, "alias"],
601
+ message: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
602
+ });
603
+ }
604
+ // Pass the full map so cross-combo rules (alias uniqueness) apply at load time
605
+ // too, not just via the management API; each combo is excluded from its own check.
606
+ for (const issue of comboConfigIssues(id, raw, config.providers, {
607
+ combos: combos as Record<string, import("../../types").OcxComboConfig>,
608
+ excludeComboId: id,
609
+ })) {
610
+ ctx.addIssue({
611
+ code: "custom",
612
+ path: ["combos", id, ...issue.path],
613
+ message: issue.message,
614
+ });
615
+ }
616
+ }
617
+ }
618
+ }
619
+ const routingProfiles = (config as { routingProfiles?: unknown }).routingProfiles;
620
+ if (routingProfiles !== undefined) {
621
+ if (!routingProfiles || typeof routingProfiles !== "object" || Array.isArray(routingProfiles)) {
622
+ ctx.addIssue({ code: "custom", path: ["routingProfiles"], message: "routingProfiles must be an object" });
623
+ } else {
624
+ for (const [id, raw] of Object.entries(routingProfiles as Record<string, unknown>)) {
625
+ for (const issue of routingProfileIssues(id, raw, {
626
+ providers: config.providers,
627
+ combos: combos as Record<string, import("../../types").OcxComboConfig> | undefined,
628
+ routingProfiles: routingProfiles as Record<string, import("../../types").OcxRoutingProfileConfig>,
629
+ codexAccountNamespaces: accountNamespaces,
630
+ }, { excludeProfileId: id })) {
631
+ ctx.addIssue({
632
+ code: "custom",
633
+ path: ["routingProfiles", id, ...issue.path],
634
+ message: issue.message,
635
+ });
636
+ }
637
+ }
638
+ }
639
+ }
640
+ });