@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.
- package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- 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
|
+
});
|