@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
isCodexAccountGenerationLive,
|
|
9
9
|
} from "./account-store";
|
|
10
10
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
-
import { isCodexAccountPaused } from "./account-pause";
|
|
12
11
|
import { ConfigMutationLockError } from "../config";
|
|
13
12
|
import { NativeProfileError } from "./native-profile-types";
|
|
14
13
|
import { isCodexAccountUsable } from "./account-usability";
|
|
@@ -41,16 +40,30 @@ import {
|
|
|
41
40
|
isDirectCallerEntitledToCodexModel,
|
|
42
41
|
resolveCodexModelEntitlements,
|
|
43
42
|
} from "./model-entitlements";
|
|
44
|
-
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
43
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
45
44
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
46
45
|
import { maskAccountId } from "../lib/privacy";
|
|
47
46
|
import { formatErrorResponse } from "../bridge";
|
|
48
|
-
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
47
|
+
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
49
48
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
50
49
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
51
50
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
52
51
|
import { retainedUtf8Bytes } from "../lib/admission";
|
|
53
52
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
53
|
+
import { getMainAccountHardLockStatus, isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
54
|
+
import {
|
|
55
|
+
captureMainAccountIdentityGeneration,
|
|
56
|
+
captureMainQuotaWriter,
|
|
57
|
+
getObservedMainQuotaIdentityKey,
|
|
58
|
+
isMainQuotaWriterLive,
|
|
59
|
+
matchesMainQuotaCredential,
|
|
60
|
+
observeMainQuotaCredential,
|
|
61
|
+
type MainQuotaWriter,
|
|
62
|
+
} from "./main-account-cache";
|
|
63
|
+
import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
|
|
64
|
+
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
65
|
+
import { getMainReserveAuthorization, isMainReserveAuthorizationLive, type MainReserveAuthorization } from "./reserve-availability";
|
|
66
|
+
import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
54
67
|
|
|
55
68
|
const CODEX_AFFINITY_COMPONENT_MAX_BYTES = 512;
|
|
56
69
|
const CODEX_APP_AFFINITY_KEY = randomBytes(32);
|
|
@@ -99,7 +112,7 @@ export function codexPoolAffinityKey(headers: Headers): string | undefined {
|
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
export type CodexAuthContext =
|
|
102
|
-
| { kind: "main"; accountId: null }
|
|
115
|
+
| { kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization }
|
|
103
116
|
| {
|
|
104
117
|
kind: "pool";
|
|
105
118
|
accountId: string;
|
|
@@ -128,6 +141,9 @@ export type CodexAuthContext =
|
|
|
128
141
|
kind: "main-pool";
|
|
129
142
|
accountId: string;
|
|
130
143
|
writerGeneration: number;
|
|
144
|
+
/** Captured before async credential work; never reconstructed after the upstream response. */
|
|
145
|
+
mainQuotaWriter?: MainQuotaWriter;
|
|
146
|
+
reserveAuthorization?: MainReserveAuthorization;
|
|
131
147
|
accessToken: string;
|
|
132
148
|
chatgptAccountId: string;
|
|
133
149
|
/** Bypass Pool selection and suppress quota/transient failover for an exact selector. */
|
|
@@ -283,6 +299,182 @@ export class CodexAccountCooldownError extends Error {
|
|
|
283
299
|
}
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
|
|
303
|
+
readonly resetAt?: number;
|
|
304
|
+
|
|
305
|
+
constructor(resetAt?: number) {
|
|
306
|
+
super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
|
|
307
|
+
this.name = "CodexMainAccountHardLockError";
|
|
308
|
+
this.resetAt = resetAt;
|
|
309
|
+
this.message = "Codex main account is blocked by the 99% main-account quota policy."
|
|
310
|
+
+ " Choose another account, wait for quota to reset, or disable codexMainAccountHardLock in Settings.";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class CodexReserveUnavailableError extends CodexAccountCooldownError {
|
|
315
|
+
constructor() {
|
|
316
|
+
super(MAIN_CODEX_ACCOUNT_ID, 0);
|
|
317
|
+
this.name = "CodexReserveUnavailableError";
|
|
318
|
+
this.message = "Codex Reserve is unavailable for this main credential."
|
|
319
|
+
+ " Use the stored main login or its matching caller credential, and retry when OpenAI grants Reserve access."
|
|
320
|
+
+ " Reserve compatibility requires the effective local Desktop authless opt-in; it cannot switch accounts automatically.";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A local unsupported-helper refusal; retain Reserve policy error mapping on delayed sends. */
|
|
325
|
+
export class CodexReserveHelperUnsupportedError extends CodexReserveUnavailableError {
|
|
326
|
+
constructor() {
|
|
327
|
+
super();
|
|
328
|
+
this.name = "CodexReserveHelperUnsupportedError";
|
|
329
|
+
this.message = CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export type CodexAuthPolicyConfig = Readonly<Pick<OcxConfig,
|
|
334
|
+
"codexMainAccountHardLock" | "codexDesktopAuthless" | "runtimeRole" | "pausedCodexAccountIds"
|
|
335
|
+
>>;
|
|
336
|
+
|
|
337
|
+
interface CodexAuthMaterializationOptions {
|
|
338
|
+
substituteMainCredential?: boolean;
|
|
339
|
+
config?: CodexAuthPolicyConfig;
|
|
340
|
+
modelId?: string;
|
|
341
|
+
/** Trusted receiving-listener admission; never inferred from request headers or config. */
|
|
342
|
+
admission?: Pick<DataPlaneAdmission, "source">;
|
|
343
|
+
signal?: AbortSignal;
|
|
344
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
345
|
+
beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function requiresReserveAuthorization(
|
|
349
|
+
config: CodexAuthPolicyConfig | undefined,
|
|
350
|
+
modelId: string | undefined,
|
|
351
|
+
admission: Pick<DataPlaneAdmission, "source"> | undefined,
|
|
352
|
+
): boolean {
|
|
353
|
+
return modelId === NATIVE_RESERVE_MODEL && !!config && isCodexReserveRequestEligible(config, admission);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function assertReserveAdmission(config: CodexAuthPolicyConfig): void {
|
|
357
|
+
if (config.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID) || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) {
|
|
358
|
+
throw new CodexReserveUnavailableError();
|
|
359
|
+
}
|
|
360
|
+
assertMainAccountPolicy(config);
|
|
361
|
+
const cooldown = getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, "reserve");
|
|
362
|
+
if (cooldown?.cooldownUntil) {
|
|
363
|
+
throw new CodexAccountCooldownError(MAIN_CODEX_ACCOUNT_ID, cooldown.cooldownUntil, cooldown.cooldownSource, cooldown.quotaScope);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function authorizeReserveCredential(
|
|
368
|
+
token: { accessToken: string; chatgptAccountId: string },
|
|
369
|
+
writer: MainQuotaWriter | undefined,
|
|
370
|
+
config: CodexAuthPolicyConfig,
|
|
371
|
+
signal?: AbortSignal,
|
|
372
|
+
existing?: MainReserveAuthorization,
|
|
373
|
+
writerGeneration = captureConfigGeneration(),
|
|
374
|
+
): Promise<MainReserveAuthorization> {
|
|
375
|
+
assertReserveAdmission(config);
|
|
376
|
+
if (!writer || !isMainQuotaWriterLive(writer)
|
|
377
|
+
|| !matchesMainQuotaCredential(token.accessToken, token.chatgptAccountId)) {
|
|
378
|
+
throw new CodexReserveUnavailableError();
|
|
379
|
+
}
|
|
380
|
+
const authorization = isMainReserveAuthorizationLive(existing, token) ? existing
|
|
381
|
+
: await getMainReserveAuthorization({
|
|
382
|
+
token, writer, signal,
|
|
383
|
+
observeOrdinaryQuota(data, capturedWriter) {
|
|
384
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, parseUsageQuota(data), writerGeneration,
|
|
385
|
+
capturedWriter, parseMainPolicyUsageQuota(data));
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
// The capability read also publishes ordinary quota. A new 99% reading or cooldown wins.
|
|
389
|
+
assertReserveAdmission(config);
|
|
390
|
+
if (signal?.aborted) throw signal.reason;
|
|
391
|
+
if (!authorization || !isMainReserveAuthorizationLive(authorization, token)) throw new CodexReserveUnavailableError();
|
|
392
|
+
return authorization;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAccountId: string } {
|
|
396
|
+
return {
|
|
397
|
+
accessToken: headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim() ?? "",
|
|
398
|
+
chatgptAccountId: headers.get("chatgpt-account-id") ?? "",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function assertMaterializedReserve(headers: Headers, ctx: CodexAuthContext, options: CodexAuthMaterializationOptions): void {
|
|
403
|
+
if (!requiresReserveAuthorization(options.config, options.modelId, options.admission)) return;
|
|
404
|
+
assertReserveAdmission(options.config!);
|
|
405
|
+
if (ctx.kind === "pool" || !isMainReserveAuthorizationLive(ctx.reserveAuthorization, selectedCodexToken(headers))) {
|
|
406
|
+
throw new CodexReserveUnavailableError();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A dispatch never renews permission: the next request may obtain a fresh bounded proof. */
|
|
411
|
+
export function createCodexReserveDispatchGuard(
|
|
412
|
+
ctx: CodexAuthContext,
|
|
413
|
+
config: CodexAuthPolicyConfig,
|
|
414
|
+
modelId: string,
|
|
415
|
+
admission?: Pick<DataPlaneAdmission, "source">,
|
|
416
|
+
terminalHelper = false,
|
|
417
|
+
): ((headers: Headers) => void) | undefined {
|
|
418
|
+
// Snapshot the resolved source value, not the caller's mutable admission object. Config stays
|
|
419
|
+
// live so policy changes remain visible after pacing and retry backoff.
|
|
420
|
+
const source = admission?.source;
|
|
421
|
+
if (modelId !== NATIVE_RESERVE_MODEL || source !== "loopback") return undefined;
|
|
422
|
+
// Only immutable request facts decide whether to install the callback. Flag/role eligibility
|
|
423
|
+
// is checked inside it, including an opt-in enabled while a send waits for pacing or WS open.
|
|
424
|
+
const ingress = Object.freeze({ source });
|
|
425
|
+
return headers => {
|
|
426
|
+
if (isCodexReserveHelperUnsupported(config, modelId, ingress, terminalHelper)) {
|
|
427
|
+
throw new CodexReserveHelperUnsupportedError();
|
|
428
|
+
}
|
|
429
|
+
assertMaterializedReserve(headers, ctx, { config, modelId, admission: ingress });
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Retry history must not turn a later local admission refusal into a network failure. */
|
|
434
|
+
export function unwrapUpstreamRetryEvidenceError(error: unknown): unknown {
|
|
435
|
+
const seen = new Set<unknown>();
|
|
436
|
+
while (error instanceof UpstreamRetryEvidenceError && !seen.has(error)) {
|
|
437
|
+
seen.add(error);
|
|
438
|
+
error = error.cause;
|
|
439
|
+
}
|
|
440
|
+
return error;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function assertMainAccountPolicy(config: Pick<OcxConfig, "codexMainAccountHardLock"> | undefined): void {
|
|
444
|
+
if (!config) return;
|
|
445
|
+
const status = getMainAccountHardLockStatus(config);
|
|
446
|
+
if (status.state === "blocked") throw new CodexMainAccountHardLockError(status.resetAt);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** No auth-file I/O: an unsigned claim alone never identifies a caller as stored main. */
|
|
450
|
+
function callerMatchesObservedMain(headers: Headers): boolean {
|
|
451
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
452
|
+
if (!bearer) return false;
|
|
453
|
+
const effectiveAccountId = headers.get("chatgpt-account-id")
|
|
454
|
+
?? extractAccountId(undefined, bearer);
|
|
455
|
+
return matchesMainQuotaCredential(bearer, effectiveAccountId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function captureObservedMainWriter(): MainQuotaWriter | undefined {
|
|
459
|
+
const identityKey = getObservedMainQuotaIdentityKey();
|
|
460
|
+
return identityKey === undefined ? undefined : {
|
|
461
|
+
identityKey,
|
|
462
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function observeSelectedMainCredential(
|
|
467
|
+
token: { accessToken: string; chatgptAccountId: string },
|
|
468
|
+
writer: MainQuotaWriter | undefined,
|
|
469
|
+
): MainQuotaWriter | undefined {
|
|
470
|
+
if (!writer) return undefined;
|
|
471
|
+
// Carry an explicitly stale writer through to quota's rejection fence; turning it into an
|
|
472
|
+
// untagged write would instead invalidate the replacement account's trusted observation.
|
|
473
|
+
if (!isMainQuotaWriterLive(writer)) return writer;
|
|
474
|
+
const observed = observeMainQuotaCredential(token.accessToken, token.chatgptAccountId);
|
|
475
|
+
return observed?.identityKey === writer.identityKey ? writer : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
286
478
|
/**
|
|
287
479
|
* Human-readable account label for a client-visible error. NEVER the raw id: the proxy
|
|
288
480
|
* supports non-loopback binds (auth-cors.ts `isApiAuthRequired` requires a token there
|
|
@@ -300,12 +492,12 @@ export function cooldownAccountLabel(accountId: string): string {
|
|
|
300
492
|
* injected `openai_base_url` in config.toml.
|
|
301
493
|
*/
|
|
302
494
|
export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSelector?: string): string {
|
|
495
|
+
if (err instanceof CodexMainAccountHardLockError || err instanceof CodexReserveUnavailableError) return err.message;
|
|
303
496
|
const until = new Date(err.cooldownUntil).toISOString();
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
: null;
|
|
497
|
+
const scopeLabels: Record<CodexQuotaScope, string> = {
|
|
498
|
+
spark: "Spark quota", shared: "shared native quota", reserve: "Reserve quota",
|
|
499
|
+
};
|
|
500
|
+
const scope = err.quotaScope ? scopeLabels[err.quotaScope] : null;
|
|
309
501
|
const selected = accountSelector
|
|
310
502
|
? `Selected Codex account selector (${accountSelector})`
|
|
311
503
|
: `Selected Codex account (${cooldownAccountLabel(err.accountId)})`;
|
|
@@ -325,7 +517,10 @@ export function cooldownErrorResponse(
|
|
|
325
517
|
): Response {
|
|
326
518
|
const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
|
|
327
519
|
const headers = new Headers(res.headers);
|
|
328
|
-
|
|
520
|
+
if (!(err instanceof CodexReserveUnavailableError)
|
|
521
|
+
&& (!(err instanceof CodexMainAccountHardLockError) || err.resetAt !== undefined)) {
|
|
522
|
+
headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
|
|
523
|
+
}
|
|
329
524
|
return new Response(res.body, { status: res.status, headers });
|
|
330
525
|
}
|
|
331
526
|
|
|
@@ -340,7 +535,9 @@ export class CodexThreadAffinityExpiredError extends Error {
|
|
|
340
535
|
}
|
|
341
536
|
|
|
342
537
|
export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown): boolean {
|
|
343
|
-
return !(cause instanceof
|
|
538
|
+
return !(cause instanceof CodexMainAccountHardLockError)
|
|
539
|
+
&& !(cause instanceof CodexReserveUnavailableError)
|
|
540
|
+
&& !(cause instanceof CodexCredentialGenerationConflictError)
|
|
344
541
|
&& !(cause instanceof CodexCredentialRefreshLockTimeoutError)
|
|
345
542
|
&& !(cause instanceof CodexCredentialRefreshBusyError)
|
|
346
543
|
&& !(cause instanceof CodexCredentialRefreshStaleError)
|
|
@@ -352,6 +549,9 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
352
549
|
}
|
|
353
550
|
|
|
354
551
|
export interface ResolveCodexAuthContextOptions {
|
|
552
|
+
admission?: Pick<DataPlaneAdmission, "source">;
|
|
553
|
+
/** Live policy owner when the routing config is a caller-specific replay snapshot. */
|
|
554
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
355
555
|
excludeAccountId?: string;
|
|
356
556
|
/** Resolve exactly this account without consulting or mutating Pool selection. */
|
|
357
557
|
accountId?: string;
|
|
@@ -389,14 +589,21 @@ export async function resolveCodexAuthContext(
|
|
|
389
589
|
options: ResolveCodexAuthContextOptions = {},
|
|
390
590
|
): Promise<CodexAuthContext> {
|
|
391
591
|
const writerGeneration = captureConfigGeneration();
|
|
592
|
+
const policy = options.codexAuthPolicy ?? config;
|
|
392
593
|
const requestScopedMainCredential = options.requestScopedMainCredential === true
|
|
393
594
|
&& hasCallerCodexBearer(headers);
|
|
394
|
-
const
|
|
595
|
+
const reserve = requiresReserveAuthorization(policy, options.modelId, options.admission);
|
|
596
|
+
if (reserve && (options.excludeAccountId !== undefined
|
|
597
|
+
|| (options.accountId !== undefined && options.accountId !== MAIN_CODEX_ACCOUNT_ID))) {
|
|
598
|
+
throw new CodexReserveUnavailableError();
|
|
599
|
+
}
|
|
600
|
+
const fixedAccountId = reserve ? MAIN_CODEX_ACCOUNT_ID : options.accountId;
|
|
395
601
|
const preserveRequestOwnedMainPin = requestScopedMainCredential
|
|
396
602
|
&& fixedAccountId === undefined
|
|
397
603
|
&& config.activeCodexAccountPinned === MAIN_CODEX_ACCOUNT_ID
|
|
398
604
|
&& isEffectiveCodexAccountPinned(config)
|
|
399
|
-
&& !
|
|
605
|
+
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
606
|
+
&& !(callerMatchesObservedMain(headers) && isMainAccountHardLocked(policy))
|
|
400
607
|
&& requestOwnedMainPinHasQuotaHeadroom(config);
|
|
401
608
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
402
609
|
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
@@ -405,6 +612,14 @@ export async function resolveCodexAuthContext(
|
|
|
405
612
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
406
613
|
const substituteStoredMain = options.substituteMainCredentialForDirect === true;
|
|
407
614
|
if (!substituteStoredMain) {
|
|
615
|
+
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
616
|
+
if (reserve) {
|
|
617
|
+
const selected = materializeCodexUpstreamAuth(headers, { kind: "main", accountId: null }, { config: policy });
|
|
618
|
+
const token = selectedCodexToken(selected);
|
|
619
|
+
const reserveAuthorization = await authorizeReserveCredential(token, captureMainQuotaWriter(token.chatgptAccountId),
|
|
620
|
+
policy, options.signal, undefined, writerGeneration);
|
|
621
|
+
return { kind: "main", accountId: null, reserveAuthorization };
|
|
622
|
+
}
|
|
408
623
|
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
409
624
|
const entitled = await (
|
|
410
625
|
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
|
|
@@ -413,6 +628,7 @@ export async function resolveCodexAuthContext(
|
|
|
413
628
|
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
414
629
|
}
|
|
415
630
|
}
|
|
631
|
+
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
416
632
|
return { kind: "main", accountId: null };
|
|
417
633
|
}
|
|
418
634
|
|
|
@@ -432,6 +648,8 @@ export async function resolveCodexAuthContext(
|
|
|
432
648
|
) {
|
|
433
649
|
throw new CodexMainProfileDrainingError();
|
|
434
650
|
}
|
|
651
|
+
if (policy.codexMainAccountHardLock === true) reconcileMainCodexAccountRuntimeState();
|
|
652
|
+
assertMainAccountPolicy(policy);
|
|
435
653
|
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
436
654
|
const entitled = entitledCodexAccountIdsForModel(
|
|
437
655
|
await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, {
|
|
@@ -444,6 +662,7 @@ export async function resolveCodexAuthContext(
|
|
|
444
662
|
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
445
663
|
}
|
|
446
664
|
}
|
|
665
|
+
assertMainAccountPolicy(policy);
|
|
447
666
|
return { kind: "main", accountId: null };
|
|
448
667
|
} finally {
|
|
449
668
|
// The short selector reservation ends here. A successful claim remains owned by
|
|
@@ -462,13 +681,17 @@ export async function resolveCodexAuthContext(
|
|
|
462
681
|
|| await (
|
|
463
682
|
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
|
|
464
683
|
)(headers, options.modelId);
|
|
465
|
-
if (callerEntitled
|
|
684
|
+
if (callerEntitled && !(callerMatchesObservedMain(headers) && isMainAccountHardLocked(policy))) {
|
|
685
|
+
return { kind: "main", accountId: null };
|
|
686
|
+
}
|
|
466
687
|
}
|
|
467
688
|
// An explicit namespace binding is stronger than the provider's default mode. It must use the
|
|
468
689
|
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
469
690
|
// A request-owned bearer is deliberately not represented as `main-pool`: Pool account ids own
|
|
470
691
|
// durable health, quota, and affinity state, while this credential exists for one request only.
|
|
471
|
-
if ((
|
|
692
|
+
if ((reserve && hasCallerCodexBearer(headers) && !options.substituteMainCredentialForDirect
|
|
693
|
+
&& (requestScopedMainCredential || mode === "direct"))
|
|
694
|
+
|| (mode === "direct" && fixedAccountId === undefined)
|
|
472
695
|
|| (requestScopedMainCredential && fixedAccountId === MAIN_CODEX_ACCOUNT_ID)) {
|
|
473
696
|
return resolveCallerOwnedMainContext();
|
|
474
697
|
}
|
|
@@ -523,6 +746,7 @@ export async function resolveCodexAuthContext(
|
|
|
523
746
|
// A pre-drain selector reserves the native identity while reconciliation and
|
|
524
747
|
// routing inspect it. Selectors arriving after the fence skip reconciliation
|
|
525
748
|
// and may still route to non-main pool accounts without touching switch state.
|
|
749
|
+
if (reserve && !nativeMainReadsForbidden && !selectionAdmission) throw new CodexMainProfileDrainingError();
|
|
526
750
|
if (!nativeMainReadsForbidden) reconcileMainCodexAccountRuntimeState();
|
|
527
751
|
const resolution = fixedAccountId !== undefined
|
|
528
752
|
? { status: "selected" as const, accountId: fixedAccountId }
|
|
@@ -577,6 +801,11 @@ export async function resolveCodexAuthContext(
|
|
|
577
801
|
if (nativeMainReadsForbidden && !options.excludeAccountId) {
|
|
578
802
|
throw new CodexMainProfileDrainingError();
|
|
579
803
|
}
|
|
804
|
+
if (!nativeMainReadsForbidden && options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
805
|
+
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
806
|
+
&& (!modelEligibleAccountIds || modelEligibleAccountIds.has(MAIN_CODEX_ACCOUNT_ID))) {
|
|
807
|
+
assertMainAccountPolicy(policy);
|
|
808
|
+
}
|
|
580
809
|
throw new CodexPoolAuthenticationError(
|
|
581
810
|
modelEligibleAccountIds === undefined
|
|
582
811
|
? undefined
|
|
@@ -586,6 +815,7 @@ export async function resolveCodexAuthContext(
|
|
|
586
815
|
);
|
|
587
816
|
}
|
|
588
817
|
accountId = selected;
|
|
818
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) assertMainAccountPolicy(policy);
|
|
589
819
|
if (accountId === MAIN_CODEX_ACCOUNT_ID && nativeMainTrafficBlocked) {
|
|
590
820
|
throw new CodexMainProfileDrainingError();
|
|
591
821
|
}
|
|
@@ -608,7 +838,7 @@ export async function resolveCodexAuthContext(
|
|
|
608
838
|
);
|
|
609
839
|
}
|
|
610
840
|
if (fixedAccountId !== undefined) {
|
|
611
|
-
if (
|
|
841
|
+
if (policy.pausedCodexAccountIds?.includes(accountId)) {
|
|
612
842
|
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
613
843
|
}
|
|
614
844
|
if (isAccountNeedsReauth(accountId)) {
|
|
@@ -662,14 +892,18 @@ export async function resolveCodexAuthContext(
|
|
|
662
892
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
663
893
|
// Main account in rotation: refresh auth.json before upstream I/O and fail closed if it vanished.
|
|
664
894
|
let token: { accessToken: string; chatgptAccountId: string } | null;
|
|
895
|
+
let mainQuotaWriter = captureObservedMainWriter();
|
|
665
896
|
try {
|
|
666
897
|
token = await (options.getValidMainAccountToken ?? getValidMainAccountToken)({
|
|
667
898
|
signal: options.signal,
|
|
668
899
|
...(options.nativeMainRefreshDependencies ?? {}),
|
|
669
900
|
});
|
|
901
|
+
if (token) mainQuotaWriter = observeSelectedMainCredential(token, mainQuotaWriter);
|
|
902
|
+
assertMainAccountPolicy(policy);
|
|
670
903
|
} catch (cause) {
|
|
671
904
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
672
905
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
906
|
+
if (cause instanceof CodexMainAccountHardLockError) throw cause;
|
|
673
907
|
if (!options.signal?.aborted && shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
674
908
|
markAccountNeedsReauth(accountId, writerGeneration);
|
|
675
909
|
}
|
|
@@ -683,10 +917,15 @@ export async function resolveCodexAuthContext(
|
|
|
683
917
|
fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
|
|
684
918
|
);
|
|
685
919
|
}
|
|
920
|
+
const reserveAuthorization = reserve
|
|
921
|
+
? await authorizeReserveCredential(token, mainQuotaWriter, policy, options.signal, undefined, writerGeneration)
|
|
922
|
+
: undefined;
|
|
686
923
|
return {
|
|
687
924
|
kind: "main-pool",
|
|
688
925
|
accountId,
|
|
689
926
|
writerGeneration,
|
|
927
|
+
mainQuotaWriter,
|
|
928
|
+
...(reserveAuthorization ? { reserveAuthorization } : {}),
|
|
690
929
|
accessToken: token.accessToken,
|
|
691
930
|
chatgptAccountId: token.chatgptAccountId,
|
|
692
931
|
...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
|
|
@@ -772,7 +1011,7 @@ export class CodexMainSubstitutionUnavailableError extends Error {
|
|
|
772
1011
|
export function materializeCodexUpstreamAuth(
|
|
773
1012
|
headers: Headers,
|
|
774
1013
|
ctx: CodexAuthContext,
|
|
775
|
-
options:
|
|
1014
|
+
options: CodexAuthMaterializationOptions = {},
|
|
776
1015
|
): Headers {
|
|
777
1016
|
const selected = new Headers();
|
|
778
1017
|
for (const name of FORWARD_HEADERS) {
|
|
@@ -782,6 +1021,11 @@ export function materializeCodexUpstreamAuth(
|
|
|
782
1021
|
if (ctx.kind === "pool" || ctx.kind === "main-pool") {
|
|
783
1022
|
selected.set("authorization", `Bearer ${ctx.accessToken}`);
|
|
784
1023
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
1024
|
+
if (ctx.kind === "main-pool") {
|
|
1025
|
+
ctx.mainQuotaWriter = observeSelectedMainCredential(ctx, ctx.mainQuotaWriter);
|
|
1026
|
+
assertMainAccountPolicy(options.config);
|
|
1027
|
+
}
|
|
1028
|
+
assertMaterializedReserve(selected, ctx, options);
|
|
785
1029
|
return selected;
|
|
786
1030
|
}
|
|
787
1031
|
if (ctx.kind === "main" && options.substituteMainCredential !== true
|
|
@@ -791,6 +1035,8 @@ export function materializeCodexUpstreamAuth(
|
|
|
791
1035
|
if (accountId) selected.set("chatgpt-account-id", accountId);
|
|
792
1036
|
}
|
|
793
1037
|
if (ctx.kind === "main" && options.substituteMainCredential === true) {
|
|
1038
|
+
if (options.config?.codexMainAccountHardLock === true) reconcileMainCodexAccountRuntimeState();
|
|
1039
|
+
const writer = captureObservedMainWriter();
|
|
794
1040
|
const stored = getMainAccountToken();
|
|
795
1041
|
// Fail BEFORE any upstream I/O. Falling through here would send the admission secret.
|
|
796
1042
|
if (!stored?.accessToken || !isMainAccountTokenLive()) {
|
|
@@ -798,20 +1044,66 @@ export function materializeCodexUpstreamAuth(
|
|
|
798
1044
|
}
|
|
799
1045
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
800
1046
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1047
|
+
observeSelectedMainCredential(stored, writer);
|
|
1048
|
+
assertMainAccountPolicy(options.config);
|
|
1049
|
+
assertMaterializedReserve(selected, ctx, options);
|
|
801
1050
|
return selected;
|
|
802
1051
|
}
|
|
1052
|
+
if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
|
|
1053
|
+
assertMaterializedReserve(selected, ctx, options);
|
|
803
1054
|
return selected;
|
|
804
1055
|
}
|
|
805
1056
|
|
|
1057
|
+
/** The model producer, not an optional context marker, decides whether a grant is required. */
|
|
1058
|
+
async function materializeReserveUpstreamAuth(
|
|
1059
|
+
headers: Headers,
|
|
1060
|
+
ctx: CodexAuthContext,
|
|
1061
|
+
options: CodexAuthMaterializationOptions,
|
|
1062
|
+
): Promise<Headers> {
|
|
1063
|
+
if (ctx.kind === "pool") throw new CodexReserveUnavailableError();
|
|
1064
|
+
const config = options.config!;
|
|
1065
|
+
assertReserveAdmission(config);
|
|
1066
|
+
const writerGeneration = ctx.kind === "main-pool" ? ctx.writerGeneration : captureConfigGeneration();
|
|
1067
|
+
const storedMain = ctx.kind === "main"
|
|
1068
|
+
&& (options.substituteMainCredential === true || !hasCallerCodexBearer(headers));
|
|
1069
|
+
let admission: CodexAccountSelectionAdmission | undefined;
|
|
1070
|
+
let writer = ctx.kind === "main-pool" ? ctx.mainQuotaWriter : undefined;
|
|
1071
|
+
try {
|
|
1072
|
+
if (storedMain) {
|
|
1073
|
+
if (isNativeMainTrafficBlocked()) throw new CodexMainProfileDrainingError();
|
|
1074
|
+
admission = options.beginCodexAccountSelection?.();
|
|
1075
|
+
if (!admission || admission.mainProfileDraining || !admission.claimMainProfile() || isNativeMainTrafficBlocked()) {
|
|
1076
|
+
throw new CodexMainProfileDrainingError();
|
|
1077
|
+
}
|
|
1078
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1079
|
+
writer = captureObservedMainWriter();
|
|
1080
|
+
assertReserveAdmission(config);
|
|
1081
|
+
}
|
|
1082
|
+
// Build the real credential first, without recursively requiring a not-yet-fetched proof.
|
|
1083
|
+
// The ordinary hard lock remains enabled, including the post-refresh check.
|
|
1084
|
+
const selected = await materializeCodexUpstreamAuthAsync(headers, ctx, {
|
|
1085
|
+
...options, modelId: undefined, substituteMainCredential: storedMain,
|
|
1086
|
+
});
|
|
1087
|
+
const token = selectedCodexToken(selected);
|
|
1088
|
+
if (ctx.kind === "main-pool") writer = ctx.mainQuotaWriter;
|
|
1089
|
+
else if (!storedMain) writer = captureMainQuotaWriter(token.chatgptAccountId);
|
|
1090
|
+
ctx.reserveAuthorization = await authorizeReserveCredential(token, writer, config, options.signal,
|
|
1091
|
+
ctx.reserveAuthorization, writerGeneration);
|
|
1092
|
+
assertMaterializedReserve(selected, ctx, options);
|
|
1093
|
+
return selected;
|
|
1094
|
+
} finally {
|
|
1095
|
+
admission?.release();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
806
1099
|
export async function materializeCodexUpstreamAuthAsync(
|
|
807
1100
|
headers: Headers,
|
|
808
1101
|
ctx: CodexAuthContext,
|
|
809
|
-
options: {
|
|
810
|
-
substituteMainCredential?: boolean;
|
|
811
|
-
signal?: AbortSignal;
|
|
812
|
-
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
813
|
-
} = {},
|
|
1102
|
+
options: CodexAuthMaterializationOptions = {},
|
|
814
1103
|
): Promise<Headers> {
|
|
1104
|
+
if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
|
|
1105
|
+
return materializeReserveUpstreamAuth(headers, ctx, options);
|
|
1106
|
+
}
|
|
815
1107
|
if (ctx.kind !== "main" || options.substituteMainCredential !== true) {
|
|
816
1108
|
return materializeCodexUpstreamAuth(headers, ctx, options);
|
|
817
1109
|
}
|
|
@@ -820,6 +1112,8 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
820
1112
|
const value = headers.get(name);
|
|
821
1113
|
if (value) selected.set(name, value);
|
|
822
1114
|
}
|
|
1115
|
+
if (options.config?.codexMainAccountHardLock === true) reconcileMainCodexAccountRuntimeState();
|
|
1116
|
+
const writer = captureObservedMainWriter();
|
|
823
1117
|
const stored = await getValidMainAccountToken({
|
|
824
1118
|
signal: options.signal,
|
|
825
1119
|
...(options.nativeMainRefreshDependencies ?? {}),
|
|
@@ -827,12 +1121,22 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
827
1121
|
if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
|
|
828
1122
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
829
1123
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1124
|
+
observeSelectedMainCredential(stored, writer);
|
|
1125
|
+
assertMainAccountPolicy(options.config);
|
|
1126
|
+
// An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
|
|
1127
|
+
assertMaterializedReserve(selected, ctx, options);
|
|
830
1128
|
return selected;
|
|
831
1129
|
}
|
|
832
1130
|
|
|
833
1131
|
/** @deprecated Prefer materializeCodexUpstreamAuth; kept for call sites without admission context. */
|
|
834
|
-
export function headersForCodexAuthContext(
|
|
835
|
-
|
|
1132
|
+
export function headersForCodexAuthContext(
|
|
1133
|
+
headers: Headers,
|
|
1134
|
+
ctx: CodexAuthContext,
|
|
1135
|
+
config?: CodexAuthPolicyConfig,
|
|
1136
|
+
modelId?: string,
|
|
1137
|
+
admission?: Pick<DataPlaneAdmission, "source">,
|
|
1138
|
+
): Headers {
|
|
1139
|
+
return materializeCodexUpstreamAuth(headers, ctx, { config, modelId, admission });
|
|
836
1140
|
}
|
|
837
1141
|
|
|
838
1142
|
export function isCodexAuthContextUsable(ctx: CodexAuthContext, config: OcxConfig): boolean {
|
|
@@ -35,7 +35,8 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
|
35
35
|
import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
|
|
36
36
|
import type { CatalogModel, RawEntry } from "./parsing";
|
|
37
37
|
import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
|
|
38
|
-
import { nativeOpenAiCapabilitySourceSlug, SELF_DESCRIBED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
38
|
+
import { nativeOpenAiCapabilitySourceSlug, SELF_DESCRIBED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
39
|
+
import { isReserveCatalogProjection } from "./reserve";
|
|
39
40
|
import { loadBundledCodexCatalog } from "./bundled";
|
|
40
41
|
import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
|
|
41
42
|
import { deriveEntry } from "./sync";
|
|
@@ -158,11 +159,14 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
158
159
|
entry.supports_reasoning_summaries = model.supportsReasoningSummaries;
|
|
159
160
|
}
|
|
160
161
|
if (model.supportsServiceTier === true) {
|
|
162
|
+
const nativeFast = model.codexForwardNativeCapabilityAlias && Array.isArray(entry.service_tiers)
|
|
163
|
+
? entry.service_tiers.find(tier => tier?.id === "priority")
|
|
164
|
+
: undefined;
|
|
161
165
|
entry.default_service_tier = null;
|
|
162
166
|
entry.service_tiers = [{
|
|
163
167
|
id: "priority",
|
|
164
168
|
name: "Fast",
|
|
165
|
-
description: model.fastTierDescription ?? "1.5x speed, increased usage",
|
|
169
|
+
description: model.fastTierDescription ?? nativeFast?.description ?? "1.5x speed, increased usage",
|
|
166
170
|
}];
|
|
167
171
|
entry.additional_speed_tiers = ["fast"];
|
|
168
172
|
}
|
|
@@ -337,6 +341,10 @@ export function clampedDefaultEffort(original: string, surviving: readonly strin
|
|
|
337
341
|
return (atOrBelow.at(-1) ?? ranked[0]!).effort;
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
function requiresExactReserveEfforts(entry: RawEntry): boolean {
|
|
345
|
+
return entry.slug === NATIVE_RESERVE_MODEL || isReserveCatalogProjection(entry);
|
|
346
|
+
}
|
|
347
|
+
|
|
340
348
|
export function clampEntryToCodexSupportedEfforts(
|
|
341
349
|
entry: RawEntry,
|
|
342
350
|
supported: ReadonlySet<string> | null,
|
|
@@ -347,6 +355,19 @@ export function clampEntryToCodexSupportedEfforts(
|
|
|
347
355
|
: null;
|
|
348
356
|
if (levels && levels.length > 0) {
|
|
349
357
|
const kept = levels.filter(level => typeof level?.effort === "string" && supported.has(level.effort));
|
|
358
|
+
if (requiresExactReserveEfforts(entry)) {
|
|
359
|
+
entry.supported_reasoning_levels = kept;
|
|
360
|
+
if (kept.length === 0) {
|
|
361
|
+
// The list-level clamp removes this incompatible row; never invent another ladder.
|
|
362
|
+
delete entry.default_reasoning_level;
|
|
363
|
+
} else if (!kept.some(level => level.effort === entry.default_reasoning_level)) {
|
|
364
|
+
entry.default_reasoning_level = clampedDefaultEffort(
|
|
365
|
+
typeof entry.default_reasoning_level === "string" ? entry.default_reasoning_level : "",
|
|
366
|
+
kept.map(level => level.effort!),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
350
371
|
entry.supported_reasoning_levels = kept.length > 0
|
|
351
372
|
? kept
|
|
352
373
|
: CODEX_REASONING_LEVELS
|
|
@@ -377,7 +398,9 @@ export function clampCatalogModelsToObservedCodexSupport(
|
|
|
377
398
|
|
|
378
399
|
const removed = new Set<string>();
|
|
379
400
|
const affected: string[] = [];
|
|
380
|
-
for (
|
|
401
|
+
for (let index = 0; index < models.length;) {
|
|
402
|
+
const entry = models[index]!;
|
|
403
|
+
const hadLadder = Array.isArray(entry.supported_reasoning_levels) && entry.supported_reasoning_levels.length > 0;
|
|
381
404
|
const before = new Set(
|
|
382
405
|
(Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
|
|
383
406
|
.flatMap(level => typeof (level as { effort?: string })?.effort === "string"
|
|
@@ -399,11 +422,14 @@ export function clampCatalogModelsToObservedCodexSupport(
|
|
|
399
422
|
: null;
|
|
400
423
|
const lost = [...before].filter(effort => !after.has(effort));
|
|
401
424
|
const defaultClamped = Boolean(beforeDefault && beforeDefault !== afterDefault);
|
|
402
|
-
|
|
425
|
+
const omitted = requiresExactReserveEfforts(entry) && hadLadder && after.size === 0;
|
|
426
|
+
if (lost.length > 0 || defaultClamped || omitted) {
|
|
403
427
|
for (const effort of lost) removed.add(effort);
|
|
404
428
|
if (defaultClamped && beforeDefault) removed.add(beforeDefault);
|
|
405
429
|
if (typeof entry.slug === "string") affected.push(entry.slug);
|
|
406
430
|
}
|
|
431
|
+
if (omitted) models.splice(index, 1);
|
|
432
|
+
else index += 1;
|
|
407
433
|
}
|
|
408
434
|
|
|
409
435
|
return {
|