@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923
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 +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountPriorityFailbackEnabled } from "./account-priority";
|
|
1
2
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
2
3
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import {
|
|
@@ -80,6 +81,7 @@ import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupport
|
|
|
80
81
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
81
82
|
import { getMainReserveAuthorization, isMainReserveAuthorizationLive, nativeUserIdClaims, type MainReserveAuthorization } from "./reserve-availability";
|
|
82
83
|
import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
84
|
+
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
87
|
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
|
|
@@ -90,7 +92,7 @@ import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
|
90
92
|
* request that already brought its own credential (#3157).
|
|
91
93
|
*/
|
|
92
94
|
function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
93
|
-
const threshold = config
|
|
95
|
+
const threshold = getEffectiveCodexAutoSwitchThreshold(config, MAIN_CODEX_ACCOUNT_ID);
|
|
94
96
|
if (threshold <= 0) return true;
|
|
95
97
|
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
|
|
96
98
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
|
|
@@ -121,6 +123,7 @@ export function requestOwnedMainPinState(
|
|
|
121
123
|
policy: CodexAuthPolicyConfig,
|
|
122
124
|
requestScopedMainCredential: boolean,
|
|
123
125
|
fixedAccountId: string | undefined,
|
|
126
|
+
quotaScope?: CodexQuotaScope,
|
|
124
127
|
): { candidate: boolean; preserve: boolean } {
|
|
125
128
|
const candidate = requestScopedMainCredential
|
|
126
129
|
&& fixedAccountId === undefined
|
|
@@ -130,7 +133,9 @@ export function requestOwnedMainPinState(
|
|
|
130
133
|
&& requestOwnedMainPinHasQuotaHeadroom(config);
|
|
131
134
|
return {
|
|
132
135
|
candidate,
|
|
133
|
-
preserve: candidate && !(callerMatchesObservedMain(headers)
|
|
136
|
+
preserve: candidate && !(callerMatchesObservedMain(headers)
|
|
137
|
+
&& (isMainAccountHardLocked(policy)
|
|
138
|
+
|| getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope)?.cooldownUntil)),
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -181,6 +186,42 @@ function poolStateEligible(
|
|
|
181
186
|
return fixedAccountId === undefined && !requestScopedMainCredential;
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Does main have a live credential for a request that carries main's own bearer?
|
|
191
|
+
*
|
|
192
|
+
* The one expression final authentication and every preview must agree on, for the same reason
|
|
193
|
+
* `poolStateEligible` is: preview exists to predict the resolution, and a preview that scores main
|
|
194
|
+
* differently hands subagent fallback a different account than the one that serves (#4850).
|
|
195
|
+
*
|
|
196
|
+
* A forwardable request-owned bearer IS main's live credential -- it is exactly what would be
|
|
197
|
+
* sent if selection named main -- so the honest answer is yes, without reading the stored
|
|
198
|
+
* credential. An effective manual pin answers yes as it always did. The answer is no while the
|
|
199
|
+
* physical identity is fenced for a different reason, because retained recovery and a profile
|
|
200
|
+
* drain must keep main out of routing even for a request holding its own bearer.
|
|
201
|
+
*/
|
|
202
|
+
export function requestOwnedMainCredentialIsLive(inputs: {
|
|
203
|
+
preserveRequestOwnedMainPin: boolean;
|
|
204
|
+
requestScopedMainCredential: boolean;
|
|
205
|
+
nativeMainTrafficBlocked: boolean;
|
|
206
|
+
mainProfileDraining: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* The caller's own credential is one of the Pool subscriptions currently in cooldown.
|
|
209
|
+
*
|
|
210
|
+
* Final authentication only. Cooldown identity is not modelled by preview and never was --
|
|
211
|
+
* `callerIsCooledPoolAccount` has no other caller -- because it decides a refusal rather than
|
|
212
|
+
* which account serves, so a preview that scores main while the resolution refuses still hands
|
|
213
|
+
* subagent fallback the right account. Passed explicitly at both preview sites so the asymmetry
|
|
214
|
+
* is stated rather than inherited from a default.
|
|
215
|
+
*/
|
|
216
|
+
callerOwnsCooledPoolSubscription: boolean;
|
|
217
|
+
}): boolean {
|
|
218
|
+
return inputs.preserveRequestOwnedMainPin
|
|
219
|
+
|| (inputs.requestScopedMainCredential
|
|
220
|
+
&& !inputs.nativeMainTrafficBlocked
|
|
221
|
+
&& !inputs.mainProfileDraining
|
|
222
|
+
&& !inputs.callerOwnsCooledPoolSubscription);
|
|
223
|
+
}
|
|
224
|
+
|
|
184
225
|
/**
|
|
185
226
|
* May this request own Pool affinity state at all?
|
|
186
227
|
*
|
|
@@ -598,6 +639,20 @@ function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAcc
|
|
|
598
639
|
};
|
|
599
640
|
}
|
|
600
641
|
|
|
642
|
+
/**
|
|
643
|
+
* The workspace account id a request-owned `main` credential materializes under, or
|
|
644
|
+
* `undefined` when the caller's headers carry none. This is the `chatgpt-account-id`
|
|
645
|
+
* `materializeCodexUpstreamAuth` would set for a caller-owned `{ kind: "main" }` context,
|
|
646
|
+
* read here without touching a credential store so a rotation gate can compare workspace
|
|
647
|
+
* scope before a send is ever built.
|
|
648
|
+
*/
|
|
649
|
+
export function callerCodexWorkspaceAccountId(headers: Headers): string | undefined {
|
|
650
|
+
const explicit = headers.get("chatgpt-account-id");
|
|
651
|
+
if (explicit) return explicit;
|
|
652
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
653
|
+
return bearer ? extractAccountId(undefined, bearer) : undefined;
|
|
654
|
+
}
|
|
655
|
+
|
|
601
656
|
function assertMaterializedReserve(headers: Headers, ctx: CodexAuthContext, options: CodexAuthMaterializationOptions): void {
|
|
602
657
|
if (!requiresReserveAuthorization(options.config, options.modelId, options.admission)) return;
|
|
603
658
|
assertReserveAdmission(options.config!);
|
|
@@ -702,6 +757,31 @@ function callerIsCooledPoolAccount(headers: Headers, config: OcxConfig, accountI
|
|
|
702
757
|
return true;
|
|
703
758
|
}
|
|
704
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Does the caller's own credential belong to a Pool subscription that is currently cooled?
|
|
762
|
+
*
|
|
763
|
+
* The cooldown fallback below asks this of the SELECTED account, which was sufficient while a
|
|
764
|
+
* request-owned bearer could not make main a candidate: the cooled account was the selection, so
|
|
765
|
+
* the question and the refusal sat at the same place. Once main takes part in ordering (#5019) the
|
|
766
|
+
* cooled account is no longer selected, and serving its own credential as main would resurrect the
|
|
767
|
+
* cooldown it is inside — so eligibility has to ask the same question of every cooled sibling.
|
|
768
|
+
*
|
|
769
|
+
* `callerIsCooledPoolAccount` fails closed on an unreadable caller identity, which is preserved
|
|
770
|
+
* here: an opaque bearer counts as owning any cooled subscription rather than escaping it.
|
|
771
|
+
*/
|
|
772
|
+
function callerOwnsAnyCooledPoolSubscription(
|
|
773
|
+
headers: Headers,
|
|
774
|
+
config: OcxConfig,
|
|
775
|
+
quotaScope: CodexQuotaScope | undefined,
|
|
776
|
+
): boolean {
|
|
777
|
+
for (const account of config.codexAccounts ?? []) {
|
|
778
|
+
if (account.id === MAIN_CODEX_ACCOUNT_ID) continue;
|
|
779
|
+
if (!getCodexQuotaHealthSnapshot(account.id, quotaScope)?.cooldownUntil) continue;
|
|
780
|
+
if (callerIsCooledPoolAccount(headers, config, account.id)) return true;
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
705
785
|
function captureObservedMainWriter(): MainQuotaWriter | undefined {
|
|
706
786
|
const identityKey = getObservedMainQuotaIdentityKey();
|
|
707
787
|
return identityKey === undefined ? undefined : {
|
|
@@ -856,15 +936,31 @@ export async function resolveCodexAuthContext(
|
|
|
856
936
|
throw new CodexReserveUnavailableError();
|
|
857
937
|
}
|
|
858
938
|
const fixedAccountId = reserve ? MAIN_CODEX_ACCOUNT_ID : options.accountId;
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
939
|
+
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
940
|
+
// Pool pins and fallback must not resurrect an observed main credential that is cooling
|
|
941
|
+
// down. Unrelated caller-owned credentials and explicit Direct keep their own policy.
|
|
942
|
+
// The identity match and scoped health read are memory-only; never probe the auth file.
|
|
943
|
+
const callerOwnedMainPoolCooldown = () => mode === "pool" && callerMatchesObservedMain(headers)
|
|
944
|
+
? getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope)
|
|
945
|
+
: null;
|
|
946
|
+
const assertCallerOwnedMainPoolNotCooled = () => {
|
|
947
|
+
const cooldown = callerOwnedMainPoolCooldown();
|
|
948
|
+
if (cooldown?.cooldownUntil) {
|
|
949
|
+
throw new CodexAccountCooldownError(
|
|
950
|
+
MAIN_CODEX_ACCOUNT_ID, cooldown.cooldownUntil, cooldown.cooldownSource, cooldown.quotaScope,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
const mainPinState = () => requestOwnedMainPinState(
|
|
955
|
+
headers, config, policy, requestScopedMainCredential, fixedAccountId, quotaScope,
|
|
956
|
+
);
|
|
957
|
+
const requestOwnedMainPinCandidate = mainPinState().candidate;
|
|
863
958
|
// During an owned startup, equality cannot be established until recovery and the
|
|
864
959
|
// memory-only policy binding finish. This read-only fence never probes a foreign home.
|
|
865
960
|
if (policy.codexMainAccountHardLock === true && requestOwnedMainPinCandidate && isMainAccountPolicyBindingPending()) {
|
|
866
961
|
throw new CodexMainProfileDrainingError();
|
|
867
962
|
}
|
|
963
|
+
const preserveRequestOwnedMainPin = () => mainPinState().preserve;
|
|
868
964
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
869
965
|
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
870
966
|
}
|
|
@@ -878,6 +974,7 @@ export async function resolveCodexAuthContext(
|
|
|
878
974
|
throw new CodexMainProfileDrainingError();
|
|
879
975
|
}
|
|
880
976
|
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
977
|
+
assertCallerOwnedMainPoolNotCooled();
|
|
881
978
|
if (reserve) {
|
|
882
979
|
const selected = materializeCodexUpstreamAuth(headers, { kind: "main", accountId: null }, { config: policy });
|
|
883
980
|
const token = selectedCodexToken(selected);
|
|
@@ -897,6 +994,7 @@ export async function resolveCodexAuthContext(
|
|
|
897
994
|
}
|
|
898
995
|
}
|
|
899
996
|
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
997
|
+
assertCallerOwnedMainPoolNotCooled();
|
|
900
998
|
return { kind: "main", accountId: null };
|
|
901
999
|
}
|
|
902
1000
|
|
|
@@ -946,13 +1044,13 @@ export async function resolveCodexAuthContext(
|
|
|
946
1044
|
// is the one exception where that exclusion is selection evidence in the opposite direction.
|
|
947
1045
|
// Validate the caller's own gated-model roster before using it, and fall through to a Pool model
|
|
948
1046
|
// detour when it lacks the grant. This branch performs no physical-main credential read.
|
|
949
|
-
if (preserveRequestOwnedMainPin) {
|
|
1047
|
+
if (preserveRequestOwnedMainPin()) {
|
|
950
1048
|
const callerEntitled = !options.modelId
|
|
951
1049
|
|| !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
952
1050
|
|| await (
|
|
953
1051
|
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
|
|
954
1052
|
)(headers, options.modelId);
|
|
955
|
-
if (callerEntitled &&
|
|
1053
|
+
if (callerEntitled && preserveRequestOwnedMainPin()) {
|
|
956
1054
|
return { kind: "main", accountId: null };
|
|
957
1055
|
}
|
|
958
1056
|
}
|
|
@@ -1001,7 +1099,21 @@ export async function resolveCodexAuthContext(
|
|
|
1001
1099
|
const nativeMainSelectionOnly = !nativeMainTrafficBlocked
|
|
1002
1100
|
&& selectionAdmission?.mainProfileDraining === true;
|
|
1003
1101
|
let accountId: string;
|
|
1004
|
-
|
|
1102
|
+
// Answering this with the manual-pin predicate made `codexAccountUnusableReason` report
|
|
1103
|
+
// `main_credential_unavailable` for every UNPINNED request, so `getEligiblePoolAccounts` never
|
|
1104
|
+
// listed main and the strategy compared only the stored accounts. With one stored sibling the
|
|
1105
|
+
// pool degraded to "stored account until it cannot serve, then main", discarding the usage,
|
|
1106
|
+
// priority and reset ordering the operator configured (#5019).
|
|
1107
|
+
const requestOwnedMainCredentialLive = () => requestOwnedMainCredentialIsLive({
|
|
1108
|
+
preserveRequestOwnedMainPin: preserveRequestOwnedMainPin(),
|
|
1109
|
+
requestScopedMainCredential,
|
|
1110
|
+
nativeMainTrafficBlocked,
|
|
1111
|
+
mainProfileDraining: selectionAdmission?.mainProfileDraining === true,
|
|
1112
|
+
// Keeps the cooled account as the selection when the caller owns it, so the refusal is still
|
|
1113
|
+
// produced by the cooldown machinery below rather than by a second rule beside it.
|
|
1114
|
+
callerOwnsCooledPoolSubscription: requestScopedMainCredential
|
|
1115
|
+
&& callerOwnsAnyCooledPoolSubscription(headers, config, quotaScope),
|
|
1116
|
+
});
|
|
1005
1117
|
try {
|
|
1006
1118
|
const excludeAccountIds = nativeMainReadsForbidden
|
|
1007
1119
|
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
@@ -1042,13 +1154,15 @@ export async function resolveCodexAuthContext(
|
|
|
1042
1154
|
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
1043
1155
|
nativeMainSelectionOnly,
|
|
1044
1156
|
isMainAccountTokenLive: requestScopedMainCredential
|
|
1045
|
-
// Main stays excluded from this request's model roster below
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
|
|
1157
|
+
// Main stays excluded from this request's model roster below, so an account-gated model
|
|
1158
|
+
// still cannot be served from a candidacy this answer creates. Everything else -- pool
|
|
1159
|
+
// eligibility, shared-state preservation, and a caller-owned pin surviving a model detour
|
|
1160
|
+
// -- is answered without reading or selecting the physical main credential.
|
|
1161
|
+
? requestOwnedMainCredentialLive
|
|
1049
1162
|
: options.isMainAccountTokenLive,
|
|
1050
1163
|
modelEligibleAccountIds,
|
|
1051
1164
|
deniedModelAccountIds,
|
|
1165
|
+
requestOwnedMainCredential: requestScopedMainCredential,
|
|
1052
1166
|
// Request-scoped and deliberately absent from `sharedStateSelectionOptions`: one
|
|
1053
1167
|
// conversation's attachments say nothing about where unrelated threads should be served.
|
|
1054
1168
|
retainAccountForUploadedFiles: options.retainAccountForUploadedFiles === true,
|
|
@@ -1098,6 +1212,16 @@ export async function resolveCodexAuthContext(
|
|
|
1098
1212
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
1099
1213
|
affinityDecision = resolution.affinity;
|
|
1100
1214
|
transientProbe = resolution.status === "selected" ? resolution.transientProbe : undefined;
|
|
1215
|
+
// Main now takes part in ordering when the request carries its own main bearer (#5019), so
|
|
1216
|
+
// selection can name it outright rather than only through the no-candidate fallback below.
|
|
1217
|
+
// Serve that selection from the credential the request arrived with: the read fence above
|
|
1218
|
+
// forbids this request from claiming, reading or reconciling the stored main profile, and a
|
|
1219
|
+
// request-owned credential owns no Pool state, so there is nothing here to claim, prime or
|
|
1220
|
+
// cool down. Hand back any trial first -- no Pool account is being sent to.
|
|
1221
|
+
if (selected === MAIN_CODEX_ACCOUNT_ID && requestScopedMainCredential) {
|
|
1222
|
+
releaseTransientProbeGrant();
|
|
1223
|
+
return await resolveCallerOwnedMainContext();
|
|
1224
|
+
}
|
|
1101
1225
|
if (!selected) {
|
|
1102
1226
|
// A retry that excluded a failed Pool account may still use the validated caller-owned
|
|
1103
1227
|
// main credential. Treating every exclusion as if main itself had failed strands a healthy
|
|
@@ -1202,13 +1326,17 @@ export async function resolveCodexAuthContext(
|
|
|
1202
1326
|
// unprimed (dashboard never opened, or startup prime was blocked). Kick a
|
|
1203
1327
|
// best-effort prime so the NEXT routing decision has real scores. This never
|
|
1204
1328
|
// blocks the current request, and the helper's single-flight guard collapses
|
|
1205
|
-
// repeated triggers into one pass.
|
|
1206
|
-
|
|
1329
|
+
// repeated triggers into one pass. Opt-in priority failback also refreshes inactive
|
|
1330
|
+
// accounts; that path has a five-minute attempt limit inside the prime helper.
|
|
1331
|
+
const priorityFailback = codexAccountPriorityFailbackEnabled(config, accountId);
|
|
1332
|
+
if (fixedAccountId === undefined && !nativeMainReadsForbidden
|
|
1333
|
+
&& (!getAccountQuota(accountId) || priorityFailback)) {
|
|
1334
|
+
const reason = priorityFailback ? "priority-failback" : "pre-route";
|
|
1207
1335
|
if (options.primeCodexPoolQuotas) {
|
|
1208
|
-
void options.primeCodexPoolQuotas(config,
|
|
1336
|
+
void options.primeCodexPoolQuotas(config, reason).catch(() => {});
|
|
1209
1337
|
} else {
|
|
1210
1338
|
import("./auth-api")
|
|
1211
|
-
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config,
|
|
1339
|
+
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, reason))
|
|
1212
1340
|
.catch(() => {});
|
|
1213
1341
|
}
|
|
1214
1342
|
}
|
|
@@ -437,7 +437,8 @@ export interface ObservedCatalogMergePolicy {
|
|
|
437
437
|
export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
|
|
438
438
|
Pick<ObservedCatalogMergePolicy, "nativeBackfillSlugs" | "unsupportedNativeEntries">
|
|
439
439
|
> = Object.freeze({
|
|
440
|
-
|
|
440
|
+
// A getter: configured natives join NATIVE_OPENAI_MODELS after this module loads.
|
|
441
|
+
get nativeBackfillSlugs() { return Object.freeze([...NATIVE_OPENAI_MODELS]); },
|
|
441
442
|
unsupportedNativeEntries: "drop",
|
|
442
443
|
});
|
|
443
444
|
|
|
@@ -444,6 +444,10 @@ export interface CatalogEffortCompatibility {
|
|
|
444
444
|
readonly affectedModels: readonly string[];
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// These parser-valid sentinels do not appear in native model ladders, so absence from an
|
|
448
|
+
// observed bundled catalog is not evidence that the selected Codex runtime rejects them.
|
|
449
|
+
const CODEX_PARSER_SENTINEL_EFFORTS = new Set(["none", "minimal"]);
|
|
450
|
+
|
|
447
451
|
/**
|
|
448
452
|
* Report which reasoning efforts in a catalog the local Codex runtime would reject, without
|
|
449
453
|
* changing anything.
|
|
@@ -463,9 +467,11 @@ export function catalogEffortCompatibility(
|
|
|
463
467
|
const unsupported = new Set<string>();
|
|
464
468
|
const affected: string[] = [];
|
|
465
469
|
for (const entry of models) {
|
|
466
|
-
const
|
|
470
|
+
const accepts = (effort: string): boolean => supported.has(effort)
|
|
471
|
+
|| CODEX_PARSER_SENTINEL_EFFORTS.has(effort);
|
|
472
|
+
const rejected = catalogEntryEfforts(entry).filter(effort => !accepts(effort));
|
|
467
473
|
const fallback = typeof entry.default_reasoning_level === "string"
|
|
468
|
-
&& !
|
|
474
|
+
&& !accepts(entry.default_reasoning_level)
|
|
469
475
|
? [entry.default_reasoning_level]
|
|
470
476
|
: [];
|
|
471
477
|
if (rejected.length === 0 && fallback.length === 0) continue;
|
|
@@ -41,6 +41,7 @@ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
|
41
41
|
import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
|
|
42
42
|
import {
|
|
43
43
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
44
|
+
NATIVE_GPT6_CONTEXT,
|
|
44
45
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
45
46
|
NATIVE_GPT6_ASTRA_MINOR_MODEL,
|
|
46
47
|
NATIVE_GPT6_ASTRA_MODEL,
|
|
@@ -52,10 +53,13 @@ import {
|
|
|
52
53
|
SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
|
|
53
54
|
SUPPORTED_NATIVE_OPENAI_SLUGS,
|
|
54
55
|
RETIRED_NATIVE_OPENAI_MODELS,
|
|
56
|
+
configuredNativeOpenAiModels,
|
|
55
57
|
hasNativeOpenAiCapabilityMetadata,
|
|
58
|
+
isConfiguredNativeOpenAiModel,
|
|
56
59
|
isNativeOpenAiCapabilityAliasModel,
|
|
57
60
|
nativeOpenAiAliasPresentation,
|
|
58
61
|
nativeOpenAiCapabilitySourceSlug,
|
|
62
|
+
subscribeConfiguredNativeOpenAiModels,
|
|
59
63
|
} from "./native-models";
|
|
60
64
|
import { cachedAvailableAccountGatedNativeModels } from "../model-entitlements";
|
|
61
65
|
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
@@ -186,15 +190,16 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
|
|
|
186
190
|
// family's measured 922,000 clamp — advertising 922,000 here over-stated the ceiling by 50k.
|
|
187
191
|
// maxInputTokens is clamped to the resolved window by nativeOpenAiMaxInputTokens, so this reads
|
|
188
192
|
// 272,000 by default and 872,000 only under the long-window opt-in.
|
|
189
|
-
[NATIVE_GPT6_ASTRA_MODEL]: {
|
|
193
|
+
[NATIVE_GPT6_ASTRA_MODEL]: { ...NATIVE_GPT6_CONTEXT },
|
|
190
194
|
// Sol and Luna ship the same 272,000 / 872,000 pair in their roster rows (probe of
|
|
191
195
|
// /backend-api/codex/models?client_version=0.155.0, 2026-09-23). Unmeasured here, so they take
|
|
192
196
|
// the row's own ceiling rather than the GPT-5.6 family's measured 922,000.
|
|
193
|
-
[NATIVE_GPT6_SOL_MODEL]: {
|
|
194
|
-
[NATIVE_GPT6_LUNA_MODEL]: {
|
|
197
|
+
[NATIVE_GPT6_SOL_MODEL]: { ...NATIVE_GPT6_CONTEXT },
|
|
198
|
+
[NATIVE_GPT6_LUNA_MODEL]: { ...NATIVE_GPT6_CONTEXT },
|
|
195
199
|
// Astra Minor borrows Astra's row, so it inherits Astra's numbers. No account we hold can reach
|
|
196
200
|
// it, so this is inheritance, not a measurement.
|
|
197
|
-
[NATIVE_GPT6_ASTRA_MINOR_MODEL]: {
|
|
201
|
+
[NATIVE_GPT6_ASTRA_MINOR_MODEL]: { ...NATIVE_GPT6_CONTEXT },
|
|
202
|
+
// Configured natives (providers.openai.models) are added at registration with the same pair.
|
|
198
203
|
};
|
|
199
204
|
|
|
200
205
|
// Snapshot rows plus roster-captured rows the snapshot lacks (see pinned-models.ts).
|
|
@@ -562,7 +567,8 @@ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
|
|
|
562
567
|
// Keyed on the SOURCE so an alias of a self-described row (gpt-6-astra-minor -> gpt-6-astra)
|
|
563
568
|
// is admitted the same way an alias of a GPT-5.6 row (Daybreak -> Sol) always was; for a
|
|
564
569
|
// self-described slug itself the source is the slug, so nothing else changes.
|
|
565
|
-
if (!sourceSlug.startsWith("gpt-5.6-") && !SELF_DESCRIBED_NATIVE_OPENAI_MODELS.has(sourceSlug)
|
|
570
|
+
if (!sourceSlug.startsWith("gpt-5.6-") && !SELF_DESCRIBED_NATIVE_OPENAI_MODELS.has(sourceSlug)
|
|
571
|
+
&& !isConfiguredNativeOpenAiModel(slug)) {
|
|
566
572
|
return undefined;
|
|
567
573
|
}
|
|
568
574
|
const source = PINNED_UPSTREAM_MODELS.get(sourceSlug);
|
|
@@ -621,6 +627,24 @@ export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
|
|
|
621
627
|
}),
|
|
622
628
|
);
|
|
623
629
|
|
|
630
|
+
// Configured natives join the three per-slug tables in place: other modules hold these exact
|
|
631
|
+
// objects, so a replacement would go unseen. Built-in ids are never configured, so a removal
|
|
632
|
+
// cannot delete a built-in row.
|
|
633
|
+
subscribeConfiguredNativeOpenAiModels((current, removed) => {
|
|
634
|
+
for (const slug of removed) {
|
|
635
|
+
PINNED_NATIVE_CAPABILITY_ENTRIES.delete(slug);
|
|
636
|
+
UPSTREAM_NATIVE_ENTRIES.delete(slug);
|
|
637
|
+
delete NATIVE_OPENAI_CONTEXT_OVERRIDES[slug];
|
|
638
|
+
}
|
|
639
|
+
for (const slug of current) {
|
|
640
|
+
const pinned = pinnedNativeCapabilityEntry(slug);
|
|
641
|
+
if (pinned) PINNED_NATIVE_CAPABILITY_ENTRIES.set(slug, pinned);
|
|
642
|
+
const upstream = upstreamNativeEntryForSlug(slug);
|
|
643
|
+
if (upstream) UPSTREAM_NATIVE_ENTRIES.set(slug, upstream);
|
|
644
|
+
NATIVE_OPENAI_CONTEXT_OVERRIDES[slug] = { ...NATIVE_GPT6_CONTEXT };
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
624
648
|
export function upstreamNativeEntry(slug: string): RawEntry | null {
|
|
625
649
|
const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
|
|
626
650
|
if (!entry) return null;
|
|
@@ -672,7 +696,9 @@ export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
|
|
|
672
696
|
export function nativeOpenAiSlugs(): string[] {
|
|
673
697
|
const live = catalogNativeSlugs();
|
|
674
698
|
const availableGated = cachedAvailableAccountGatedNativeModels();
|
|
675
|
-
const candidates = live.length > 0
|
|
699
|
+
const candidates = live.length > 0
|
|
700
|
+
? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels()])
|
|
701
|
+
: NATIVE_OPENAI_MODELS;
|
|
676
702
|
return candidates.filter(slug => (
|
|
677
703
|
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug)
|
|
678
704
|
));
|
|
@@ -881,5 +907,5 @@ function catalogNativeSlugs(): string[] {
|
|
|
881
907
|
export function listCatalogNativeSlugs(): string[] {
|
|
882
908
|
// Ensure documented additions (e.g. gpt-6-astra) appear even when the bundled catalog
|
|
883
909
|
// predates the slug — mirrors nativeOpenAiSlugs() which already merges them for /v1/models.
|
|
884
|
-
return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]);
|
|
910
|
+
return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS, ...configuredNativeOpenAiModels()]);
|
|
885
911
|
}
|
|
@@ -48,6 +48,17 @@ export const NATIVE_GPT6_LUNA_MODEL = "gpt-6-luna";
|
|
|
48
48
|
*/
|
|
49
49
|
export const NATIVE_GPT6_ASTRA_MINOR_MODEL = "gpt-6-astra-minor";
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Context pair every GPT-6 native ships in its upstream row: a 272,000-token default window
|
|
53
|
+
* against an 872,000-token ceiling (and input limit) reached only through the long-window opt-in.
|
|
54
|
+
* The built-in GPT-6 rows and every configured native (below) inherit this one value.
|
|
55
|
+
*/
|
|
56
|
+
export const NATIVE_GPT6_CONTEXT: Readonly<{ contextWindow: number; maxContextWindow: number; maxInputTokens: number }> =
|
|
57
|
+
Object.freeze({ contextWindow: 272_000, maxContextWindow: 872_000, maxInputTokens: 872_000 });
|
|
58
|
+
|
|
59
|
+
/** Pinned row a configured native borrows its capability metadata from. */
|
|
60
|
+
export const CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL = NATIVE_GPT6_SOL_MODEL;
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* Native ChatGPT/Codex ids whose availability is proven per authenticated account.
|
|
53
64
|
*
|
|
@@ -132,7 +143,7 @@ export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
|
|
|
132
143
|
);
|
|
133
144
|
|
|
134
145
|
export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean {
|
|
135
|
-
return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug);
|
|
146
|
+
return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug) || configuredNativeSlugs.has(slug);
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
/**
|
|
@@ -151,7 +162,8 @@ export function hasNativeOpenAiCapabilityMetadata(slug: string): boolean {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
154
|
-
return NATIVE_OPENAI_CAPABILITY_SOURCES[slug]
|
|
165
|
+
return NATIVE_OPENAI_CAPABILITY_SOURCES[slug]
|
|
166
|
+
?? (configuredNativeSlugs.has(slug) ? CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL : slug);
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
/**
|
|
@@ -171,7 +183,22 @@ export const NATIVE_OPENAI_ALIAS_PRESENTATION: Readonly<Record<string, { display
|
|
|
171
183
|
});
|
|
172
184
|
|
|
173
185
|
export function nativeOpenAiAliasPresentation(slug: string): { displayName: string; description: string } | undefined {
|
|
174
|
-
return NATIVE_OPENAI_ALIAS_PRESENTATION[slug]
|
|
186
|
+
return NATIVE_OPENAI_ALIAS_PRESENTATION[slug]
|
|
187
|
+
?? (configuredNativeSlugs.has(slug) ? configuredNativePresentation(slug) : undefined);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `gpt-6-nova` -> `GPT-6-Nova`, the same casing upstream uses for its own GPT-6 rows. */
|
|
191
|
+
export function configuredNativeOpenAiDisplayName(slug: string): string {
|
|
192
|
+
return slug.split("-").map((part, index) => (
|
|
193
|
+
index === 0 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1)
|
|
194
|
+
)).join("-");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function configuredNativePresentation(slug: string): { displayName: string; description: string } {
|
|
198
|
+
return {
|
|
199
|
+
displayName: configuredNativeOpenAiDisplayName(slug),
|
|
200
|
+
description: "OpenAI native model added through providers.openai.models; uses GPT-6-Sol capabilities.",
|
|
201
|
+
};
|
|
175
202
|
}
|
|
176
203
|
|
|
177
204
|
/**
|
|
@@ -190,17 +217,87 @@ export function nativeOpenAiAliasPresentation(slug: string): { displayName: stri
|
|
|
190
217
|
*
|
|
191
218
|
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
|
|
192
219
|
*/
|
|
193
|
-
|
|
220
|
+
const BUILT_IN_NATIVE_OPENAI_MODELS: readonly string[] = Object.freeze([
|
|
194
221
|
"gpt-5.5",
|
|
195
222
|
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
|
196
223
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
197
224
|
NATIVE_GPT6_ASTRA_MODEL,
|
|
198
225
|
NATIVE_GPT6_SOL_MODEL, NATIVE_GPT6_LUNA_MODEL,
|
|
199
226
|
NATIVE_GPT6_ASTRA_MINOR_MODEL,
|
|
200
|
-
];
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The built-in list plus every configured native, appended in config order. The array and the Set
|
|
231
|
+
* below are shared by reference across the catalog, `/v1/models` and the dashboard, so
|
|
232
|
+
* registration edits them in place rather than replacing them.
|
|
233
|
+
*/
|
|
234
|
+
export const NATIVE_OPENAI_MODELS: string[] = [...BUILT_IN_NATIVE_OPENAI_MODELS];
|
|
201
235
|
|
|
202
236
|
export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
|
|
203
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Configured natives: bare `gpt-*` ids an operator lists under `providers.openai.models` on the
|
|
240
|
+
* canonical Codex forward provider, so a new upstream GPT model needs a config entry rather than a
|
|
241
|
+
* release — the way a Claude id listed under `providers.anthropic.models` already works.
|
|
242
|
+
*
|
|
243
|
+
* A configured native borrows `gpt-6-sol`'s pinned row (ladder, modalities, instructions, speed
|
|
244
|
+
* tiers) under its own generated name, uses the GPT-6 272k/872k context pair, and is never
|
|
245
|
+
* account-gated. Filtering the config lives in `src/config/derived-registries.ts`; this module stays
|
|
246
|
+
* import-free because the GUI bundles it. Registration runs inside `loadConfig` and every config
|
|
247
|
+
* persist/reconcile path, so any process that loads config — `ocx ensure` included — sees it.
|
|
248
|
+
*/
|
|
249
|
+
const configuredNativeSlugs = new Set<string>();
|
|
250
|
+
type ConfiguredNativeListener = (current: readonly string[], removed: readonly string[]) => void;
|
|
251
|
+
const configuredNativeListeners: ConfiguredNativeListener[] = [];
|
|
252
|
+
|
|
253
|
+
const CONFIGURED_NATIVE_SLUG = /^gpt-[a-z0-9][a-z0-9.-]*$/;
|
|
254
|
+
|
|
255
|
+
/** Whether a bare id may become a configured native (shape, not built in, not retired or reserve). */
|
|
256
|
+
export function isEligibleConfiguredNativeOpenAiModel(id: string): boolean {
|
|
257
|
+
return CONFIGURED_NATIVE_SLUG.test(id)
|
|
258
|
+
&& !BUILT_IN_NATIVE_OPENAI_MODELS.includes(id)
|
|
259
|
+
&& !RETIRED_NATIVE_OPENAI_MODELS.has(id)
|
|
260
|
+
&& id !== NATIVE_RESERVE_MODEL;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function configuredNativeOpenAiModels(): readonly string[] {
|
|
264
|
+
return [...configuredNativeSlugs];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function isConfiguredNativeOpenAiModel(slug: string): boolean {
|
|
268
|
+
return configuredNativeSlugs.has(slug);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Replace the configured set. Ineligible ids are ignored; built-in entries are never touched. */
|
|
272
|
+
export function setConfiguredNativeOpenAiModels(ids: readonly string[]): void {
|
|
273
|
+
const next = [...new Set(ids.filter(isEligibleConfiguredNativeOpenAiModel))];
|
|
274
|
+
const previous = [...configuredNativeSlugs];
|
|
275
|
+
if (next.length === previous.length && next.every((id, index) => id === previous[index])) return;
|
|
276
|
+
const removed = previous.filter(id => !next.includes(id));
|
|
277
|
+
for (const id of previous) {
|
|
278
|
+
configuredNativeSlugs.delete(id);
|
|
279
|
+
SUPPORTED_NATIVE_OPENAI_SLUGS.delete(id);
|
|
280
|
+
const index = NATIVE_OPENAI_MODELS.indexOf(id);
|
|
281
|
+
if (index >= 0) NATIVE_OPENAI_MODELS.splice(index, 1);
|
|
282
|
+
}
|
|
283
|
+
for (const id of next) {
|
|
284
|
+
configuredNativeSlugs.add(id);
|
|
285
|
+
SUPPORTED_NATIVE_OPENAI_SLUGS.add(id);
|
|
286
|
+
NATIVE_OPENAI_MODELS.push(id);
|
|
287
|
+
}
|
|
288
|
+
for (const listener of configuredNativeListeners) listener(next, removed);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Keep derived tables in step; the listener runs immediately with the current set. */
|
|
292
|
+
export function subscribeConfiguredNativeOpenAiModels(listener: ConfiguredNativeListener): void {
|
|
293
|
+
configuredNativeListeners.push(listener);
|
|
294
|
+
listener(configuredNativeOpenAiModels(), []);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function resetConfiguredNativeOpenAiModelsForTests(): void {
|
|
298
|
+
setConfiguredNativeOpenAiModels([]);
|
|
299
|
+
}
|
|
300
|
+
|
|
204
301
|
/**
|
|
205
302
|
* Natives this runtime used to ship that upstream has since retired.
|
|
206
303
|
*
|
|
@@ -194,6 +194,8 @@ export const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set([
|
|
|
194
194
|
* keep appearing with its capabilities broken. Excluding the slug is what actually takes
|
|
195
195
|
* it out of the routed catalog.
|
|
196
196
|
*/
|
|
197
|
+
"deepseek/deepseek-v4-pro",
|
|
198
|
+
"opencode-go/deepseek-v4-pro",
|
|
197
199
|
"command-code/deepseek-deepseek-v4-pro",
|
|
198
200
|
"commandcode/deepseek-deepseek-v4-pro",
|
|
199
201
|
"orcarouter/deepseek-deepseek-v4-pro",
|
|
@@ -56,6 +56,7 @@ import { cursorLiveRosterScope, recordLiveCursorClaudeModels, recordLiveCursorMa
|
|
|
56
56
|
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
57
57
|
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
58
58
|
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
|
|
59
|
+
import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
|
|
59
60
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
60
61
|
import {
|
|
61
62
|
COMBO_NAMESPACE,
|
|
@@ -216,11 +217,15 @@ export async function fetchProviderModelsWithAuth(
|
|
|
216
217
|
return observed(configured, "authoritative");
|
|
217
218
|
}
|
|
218
219
|
const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
219
|
-
? prov.authMode === "oauth" &&
|
|
220
|
+
? prov.authMode === "oauth" && (
|
|
221
|
+
effectiveGoogleMode(name, prov) === "cloud-code-assist"
|
|
222
|
+
|| prov.adapter === "devin"
|
|
223
|
+
)
|
|
220
224
|
? await getValidAccessTokenSnapshot(name)
|
|
221
225
|
.then(snapshot => ({
|
|
222
226
|
apiKey: snapshot.accessToken,
|
|
223
227
|
observed: false,
|
|
228
|
+
...(snapshot.apiBaseUrl ? { oauthApiBaseUrl: snapshot.apiBaseUrl } : {}),
|
|
224
229
|
...(snapshot.projectId ? { oauthProjectId: snapshot.projectId } : {}),
|
|
225
230
|
}))
|
|
226
231
|
.catch(() => ({ apiKey: undefined, observed: false }))
|
|
@@ -289,10 +294,13 @@ export async function fetchProviderModelsWithAuth(
|
|
|
289
294
|
}
|
|
290
295
|
if (prov.adapter === "devin") {
|
|
291
296
|
if (!apiKey) return observed(configured, "degraded");
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
const
|
|
297
|
+
// Both the credential and its validated tenant destination own this roster.
|
|
298
|
+
// The registered Devin route ignores a saved baseUrl override; discovery must
|
|
299
|
+
// use that same fixed destination when the stored tenant URL is invalid.
|
|
300
|
+
const configuredBase = name === "devin" ? getProviderRegistryEntry(name)?.baseUrl ?? prov.baseUrl : prov.baseUrl;
|
|
301
|
+
const destination = resolveDevinApiBaseUrl(auth.oauthApiBaseUrl ?? configuredBase);
|
|
302
|
+
const authorityIdentity = createHash("sha256")
|
|
303
|
+
.update(JSON.stringify([apiKey, destination])).digest("hex");
|
|
296
304
|
const cachedDevin = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
|
|
297
305
|
if (cachedDevin) {
|
|
298
306
|
return observed(
|
|
@@ -309,7 +317,12 @@ export async function fetchProviderModelsWithAuth(
|
|
|
309
317
|
"degraded",
|
|
310
318
|
);
|
|
311
319
|
}
|
|
312
|
-
|
|
320
|
+
// The OAuth snapshot owns both values: never combine one account's durable
|
|
321
|
+
// key with the registry's default host or another account's tenant host.
|
|
322
|
+
const liveResult = await fetchDevinUsableModels({
|
|
323
|
+
apiKey,
|
|
324
|
+
baseUrl: destination,
|
|
325
|
+
});
|
|
313
326
|
if (liveResult.ok) {
|
|
314
327
|
// Live catalog is the source of truth — use the discovered base models
|
|
315
328
|
// directly, not a filtered subset of the static seed.
|