@bitkyc08/opencodex 2.80.0 → 2.81.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/README.md +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +1 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +3 -2
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +320 -184
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +4 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +16 -11
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +27 -5
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -146,6 +146,11 @@ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxCon
|
|
|
146
146
|
return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export interface MainAccountInfoProbeOptions {
|
|
150
|
+
/** Metadata-only renewal never sets or clears the traffic reauth quarantine. */
|
|
151
|
+
passive?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
|
|
150
155
|
|
|
151
156
|
export async function retryMainAccountInfoIfIdentityChanged(
|
|
@@ -154,12 +159,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
|
|
|
154
159
|
nativeMainLease: AdmissionLease,
|
|
155
160
|
explicitRefresh: boolean,
|
|
156
161
|
paced: boolean,
|
|
162
|
+
options: MainAccountInfoProbeOptions = {},
|
|
157
163
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
158
164
|
const currentAccountId = getMainChatgptAccountId();
|
|
159
165
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
160
166
|
reconcileMainCodexAccountRuntimeState();
|
|
161
167
|
return retriesRemaining > 0
|
|
162
|
-
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
|
|
168
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced, options)
|
|
163
169
|
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -171,6 +177,7 @@ export async function fetchMainAccountInfoAttempt(
|
|
|
171
177
|
explicitRefresh: boolean = forceRefresh,
|
|
172
178
|
postReset = false,
|
|
173
179
|
config?: OcxConfig,
|
|
180
|
+
options: MainAccountInfoProbeOptions = {},
|
|
174
181
|
): Promise<MainAccountInfoFetchResult> {
|
|
175
182
|
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
176
183
|
if (!nativeMainLease) {
|
|
@@ -184,7 +191,7 @@ export async function fetchMainAccountInfoAttempt(
|
|
|
184
191
|
try {
|
|
185
192
|
const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
|
|
186
193
|
const operation = async () => ({
|
|
187
|
-
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
|
|
194
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced, options),
|
|
188
195
|
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
189
196
|
});
|
|
190
197
|
if (nativeMainSharedClaimHeld) return await operation();
|
|
@@ -225,6 +232,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
225
232
|
/** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
|
|
226
233
|
postReset = false,
|
|
227
234
|
paced = true,
|
|
235
|
+
options: MainAccountInfoProbeOptions = {},
|
|
228
236
|
): Promise<MainAccountInfoFetchResult> {
|
|
229
237
|
const writerGeneration = captureConfigGeneration();
|
|
230
238
|
reconcileMainCodexAccountRuntimeState();
|
|
@@ -287,7 +295,16 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
287
295
|
const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
|
|
288
296
|
if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
289
297
|
credentialChecked: true, hasCredential: true };
|
|
290
|
-
|
|
298
|
+
// Single-flight shares evidence, but each caller retains its own auth behavior.
|
|
299
|
+
if (!options.passive && admission.result.terminalAuthFailure) {
|
|
300
|
+
const diagnosticStillLive = admission.result.quotaRefreshGeneration !== undefined
|
|
301
|
+
&& isMainAccountIdentityGenerationLive(admission.result.quotaRefreshGeneration);
|
|
302
|
+
clearMainAccountInfoCache();
|
|
303
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
304
|
+
return { ...admission.result, ...(diagnosticStillLive
|
|
305
|
+
? { quotaRefreshGeneration: captureMainAccountIdentityGeneration() } : {}) };
|
|
306
|
+
}
|
|
307
|
+
if (!options.passive && explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
|
|
291
308
|
|| admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
292
309
|
return admission.result;
|
|
293
310
|
}
|
|
@@ -297,13 +314,13 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
297
314
|
if (!resp.ok) {
|
|
298
315
|
const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
299
316
|
const terminalAuthFailure = authFailure.terminal;
|
|
300
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
317
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
301
318
|
if (retried) return retried;
|
|
302
319
|
if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
|
|
303
320
|
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
304
321
|
credentialChecked: true, hasCredential: true };
|
|
305
322
|
}
|
|
306
|
-
if (terminalAuthFailure) {
|
|
323
|
+
if (terminalAuthFailure && !options.passive) {
|
|
307
324
|
// Account for this attempt's own synchronous invalidation, never prior external drift.
|
|
308
325
|
const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
|
|
309
326
|
clearMainAccountInfoCache();
|
|
@@ -324,7 +341,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
324
341
|
quotaPhase = "body";
|
|
325
342
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
326
343
|
quotaPhase = "publish";
|
|
327
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
344
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
328
345
|
if (retried) return retried;
|
|
329
346
|
quotaPhase = "decode";
|
|
330
347
|
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
@@ -376,7 +393,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
376
393
|
// never settled and the dashboard kept showing nothing — the symptom #327 reported.
|
|
377
394
|
// An explicit refresh is an operator asking to re-evaluate, normally right after
|
|
378
395
|
// signing in again, so it stays authoritative.
|
|
379
|
-
if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
396
|
+
if (explicitRefresh && !options.passive) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
380
397
|
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
381
398
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
382
399
|
setMainAccountPlan(result.plan);
|
|
@@ -398,7 +415,7 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
398
415
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
399
416
|
};
|
|
400
417
|
} catch (error) {
|
|
401
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
418
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
|
|
402
419
|
if (retried) return retried;
|
|
403
420
|
if (!credentialIsCurrent()) {
|
|
404
421
|
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "../account-credit-use";
|
|
2
2
|
import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
|
|
3
3
|
import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
|
|
4
4
|
import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
|
|
5
5
|
import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
|
|
6
|
-
import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
6
|
+
import { getAccountQuota, getMainPolicyQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
7
|
+
import { isCodexAccountPaused } from "../account-pause";
|
|
8
|
+
import { hasSpendableCodexCredits } from "../quota-types";
|
|
7
9
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
8
10
|
import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
|
|
9
11
|
import { readCodexTokens } from "../auth-collision";
|
|
@@ -73,17 +75,33 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
73
75
|
return cooldownRecoveryInFlight;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
const MAIN_CREDIT_RENEWAL_AGE_MS = 3 * 60_000;
|
|
79
|
+
|
|
76
80
|
let mainHardLockRecoveryInFlight: Promise<void> | null = null;
|
|
77
81
|
let mainHardLockRecoveryAttempt: { identity: number; credential: number; after: number; delay: number } | undefined;
|
|
78
82
|
|
|
79
83
|
/** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
|
|
80
84
|
export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
|
|
81
85
|
if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
|
|
86
|
+
/** Schedule a real credit observation while leaving actual-time admission unchanged. */
|
|
87
|
+
const creditRecoveryNeeded = (): boolean => {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const quota = getMainPolicyQuota();
|
|
90
|
+
const observedAt = quota?.credits?.observedAt;
|
|
91
|
+
// getMainPolicyQuota binds evidence to the observed physical identity. Only renew
|
|
92
|
+
// valid formerly spendable funds; admission retains the actual five-minute clock.
|
|
93
|
+
return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
94
|
+
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
95
|
+
&& codexUsageLimitResetAt(quota, undefined, now) !== undefined
|
|
96
|
+
&& observedAt !== undefined && observedAt > 0
|
|
97
|
+
&& now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
|
|
98
|
+
&& hasSpendableCodexCredits(quota, observedAt);
|
|
99
|
+
};
|
|
82
100
|
const status = getMainAccountHardLockStatus(config);
|
|
83
|
-
if (status.state !== "blocked") { mainHardLockRecoveryAttempt = undefined; return; }
|
|
101
|
+
if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
|
|
84
102
|
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
85
103
|
// A predicted reset is a scheduling hint, never proof that the hard lock can be lifted.
|
|
86
|
-
if (status.resetAt !== undefined && status.resetAt > Date.now()) return;
|
|
104
|
+
if (status.state === "blocked" && status.resetAt !== undefined && status.resetAt > Date.now()) return;
|
|
87
105
|
// Reconcile the owned physical credential before skipping work for an older one.
|
|
88
106
|
reconcileMainCodexAccountRuntimeState();
|
|
89
107
|
const physical = readCodexTokens();
|
|
@@ -98,9 +116,10 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
98
116
|
if (!lease) return;
|
|
99
117
|
mainHardLockRecoveryInFlight = (async () => {
|
|
100
118
|
reconcileMainCodexAccountRuntimeState();
|
|
101
|
-
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
119
|
+
if ((getMainAccountHardLockStatus(config).state !== "blocked" && !creditRecoveryNeeded())
|
|
102
120
|
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
103
121
|
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
122
|
+
const passive = getMainAccountHardLockStatus(config).state !== "blocked";
|
|
104
123
|
try {
|
|
105
124
|
// Refresh can require an exclusive credential claim: never hold WHAM's shared
|
|
106
125
|
// claim while obtaining a valid token. The runtime lease spans both operations.
|
|
@@ -111,7 +130,12 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
111
130
|
// Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
|
|
112
131
|
return;
|
|
113
132
|
}
|
|
114
|
-
|
|
133
|
+
const currentStatus = getMainAccountHardLockStatus(config);
|
|
134
|
+
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
|
|
135
|
+
|| !isMainAccountIdentityGenerationLive(identityGeneration)
|
|
136
|
+
|| (passive && !creditRecoveryNeeded())
|
|
137
|
+
|| (currentStatus.state === "blocked" && currentStatus.resetAt !== undefined
|
|
138
|
+
&& currentStatus.resetAt > Date.now())) return;
|
|
115
139
|
const credential = getMainQuotaCredentialGeneration();
|
|
116
140
|
// Reconcile the physical bearer before consulting the previous credential's delay.
|
|
117
141
|
if (previous?.identity === identityGeneration && previous.credential === credential
|
|
@@ -123,7 +147,7 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
123
147
|
after: currentQueryAfter };
|
|
124
148
|
return;
|
|
125
149
|
}
|
|
126
|
-
const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
|
|
150
|
+
const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config, { passive });
|
|
127
151
|
// Never charge a replacement credential for a late result from its predecessor.
|
|
128
152
|
if (isMainAccountIdentityGenerationLive(identityGeneration)
|
|
129
153
|
&& credential === getMainQuotaCredentialGeneration()) {
|
|
@@ -134,11 +158,11 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
134
158
|
const authStatus = result.quotaRefresh.status === "http_error"
|
|
135
159
|
? result.quotaRefresh.httpStatus : undefined;
|
|
136
160
|
const transientAuth = (authStatus === 401 || authStatus === 403)
|
|
137
|
-
&& !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
161
|
+
&& !result.terminalAuthFailure && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
138
162
|
if (transientAuth) mainHardLockRecoveryAttempt = undefined;
|
|
139
163
|
else {
|
|
140
164
|
const delay = nextQuotaQueryDelay(previousDelay || undefined);
|
|
141
|
-
mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked"
|
|
165
|
+
mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked" || creditRecoveryNeeded()
|
|
142
166
|
? { identity: identityGeneration, credential, delay, after: Math.max(Date.now() + delay, queryAfter) }
|
|
143
167
|
: undefined;
|
|
144
168
|
}
|
|
@@ -495,6 +495,7 @@ export async function fetchPoolAccountQuota(
|
|
|
495
495
|
validatePending = false,
|
|
496
496
|
afterDispatchSequence?: number,
|
|
497
497
|
recoveryProbeNow?: number,
|
|
498
|
+
explicitRefresh = false,
|
|
498
499
|
): Promise<PoolQuotaResult> {
|
|
499
500
|
const existing = getAccountQuota(accountId);
|
|
500
501
|
if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
|
|
@@ -504,10 +505,23 @@ export async function fetchPoolAccountQuota(
|
|
|
504
505
|
credentialGeneration: readCodexAccountRecord(accountId)?.generation,
|
|
505
506
|
};
|
|
506
507
|
}
|
|
508
|
+
const record = readCodexAccountRecord(accountId);
|
|
509
|
+
// The token endpoint already called this grant revoked or expired. Only a re-login or a
|
|
510
|
+
// completed validation rewrites the record and drops the marker, so until then a passive read
|
|
511
|
+
// (dashboard poll, listing, priming, recovery probe) would only POST the dead refresh token again
|
|
512
|
+
// and log the same 401. Report the stored verdict the failed refresh would have produced. An
|
|
513
|
+
// operator's explicit refresh command from any principal, a dashboard validation, and a post-reset
|
|
514
|
+
// readback keep their probe, and a source-linked credential spends no grant of its own, so none
|
|
515
|
+
// of them is held here.
|
|
516
|
+
if (!explicitRefresh && !validatePending && afterDispatchSequence === undefined && record && record.deletedAt == null
|
|
517
|
+
&& record.credential && !record.credential.sourceAuthPath
|
|
518
|
+
&& record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed") {
|
|
519
|
+
markAccountNeedsReauth(accountId, captureConfigGeneration(), record.generation);
|
|
520
|
+
return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: record.generation };
|
|
521
|
+
}
|
|
507
522
|
// A token refresh may increment the generation (and rotate the refresh token) before WHAM
|
|
508
523
|
// completes. Join a flight whose starting or resolved generation is still current, but let a
|
|
509
524
|
// replacement credential with the same pool id start its own request.
|
|
510
|
-
const record = readCodexAccountRecord(accountId);
|
|
511
525
|
const flights = poolQuotaRefreshInFlight.get(accountId);
|
|
512
526
|
const current = flights && [...flights].find(flight => {
|
|
513
527
|
const generation = flight.state.resolvedCredentialGeneration
|
|
@@ -43,6 +43,7 @@ export async function handleCodexAuthAPI(
|
|
|
43
43
|
// required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
|
|
44
44
|
return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
|
|
45
45
|
validatePending: principal === "gui-session",
|
|
46
|
+
explicitRefresh: true,
|
|
46
47
|
}) });
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCodexControlPlaneModel } from "../control-plane-models";
|
|
1
2
|
import { redactSecretString } from "../../lib/redact";
|
|
2
3
|
import type { OcxConfig } from "../../types";
|
|
3
4
|
import { encodeRoutedModelId } from "../../providers/slug-codec";
|
|
@@ -34,6 +35,7 @@ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "
|
|
|
34
35
|
|
|
35
36
|
/** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
|
|
36
37
|
function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
38
|
+
if (isCodexControlPlaneModel(entry.slug)) return false;
|
|
37
39
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
38
40
|
return slug.includes("/")
|
|
39
41
|
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
|
|
@@ -57,6 +59,7 @@ function clearAutoReviewOverrideValue(entry: RawEntry): void {
|
|
|
57
59
|
* Returns the stamped values when the observed rows match that shape.
|
|
58
60
|
*/
|
|
59
61
|
function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
|
|
62
|
+
observedModels = observedModels.filter(entry => !isCodexControlPlaneModel(entry?.slug));
|
|
60
63
|
if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
|
|
61
64
|
const configuredValues = new Set(observedModels.flatMap(entry => {
|
|
62
65
|
const value = entry?.auto_review_model_override;
|
|
@@ -90,7 +93,7 @@ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readon
|
|
|
90
93
|
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
91
94
|
if (legacyStamp === undefined) return;
|
|
92
95
|
for (const entry of models) {
|
|
93
|
-
if (!entry || typeof entry !== "object") continue;
|
|
96
|
+
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
|
|
94
97
|
const current = entry.auto_review_model_override;
|
|
95
98
|
if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
|
|
96
99
|
&& typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
|
|
@@ -107,7 +110,7 @@ function clearAutoReviewModelOverride(
|
|
|
107
110
|
): void {
|
|
108
111
|
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
109
112
|
for (const entry of models) {
|
|
110
|
-
if (!entry || typeof entry !== "object") continue;
|
|
113
|
+
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
|
|
111
114
|
const current = entry.auto_review_model_override;
|
|
112
115
|
if (isRoutedCatalogEntry(entry)
|
|
113
116
|
|| (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
|
|
@@ -188,6 +191,7 @@ function preserveNativeAutoReviewModelOverrides(
|
|
|
188
191
|
|
|
189
192
|
/** Stamp a root-derived override and mark native rows so later root removal is durable. */
|
|
190
193
|
function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
194
|
+
if (isCodexControlPlaneModel(entry.slug)) return;
|
|
191
195
|
if (!isRoutedCatalogEntry(entry)) {
|
|
192
196
|
const previous = rootAutoReviewStamp(entry);
|
|
193
197
|
const current = entry.auto_review_model_override;
|
|
@@ -399,7 +403,7 @@ function applyRootSelectorToRemaining(
|
|
|
399
403
|
): AutoReviewModelOverrideResult {
|
|
400
404
|
const clearRemaining = (): void => {
|
|
401
405
|
for (const entry of models) {
|
|
402
|
-
if (!entry || providerStamped.has(entry)) continue;
|
|
406
|
+
if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
|
|
403
407
|
// Native rows written by releases before the root marker cannot be told apart from upstream
|
|
404
408
|
// values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
|
|
405
409
|
// uniform signature still recognizes before provider plans land, because provider stamping
|
|
@@ -427,7 +431,7 @@ function applyRootSelectorToRemaining(
|
|
|
427
431
|
return "unresolved";
|
|
428
432
|
}
|
|
429
433
|
for (const entry of models) {
|
|
430
|
-
if (!entry || providerStamped.has(entry)) continue;
|
|
434
|
+
if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
|
|
431
435
|
stampRootAutoReviewOverride(entry, trimmed);
|
|
432
436
|
}
|
|
433
437
|
return "applied";
|
|
@@ -449,7 +453,7 @@ export function applyConfiguredAutoReviewModelOverride(
|
|
|
449
453
|
const { plans, failure } = buildProviderReviewPlans(models, config);
|
|
450
454
|
const providerStamped = new Set<RawEntry>();
|
|
451
455
|
for (const entry of models) {
|
|
452
|
-
if (!entry || typeof entry !== "object") continue;
|
|
456
|
+
if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
|
|
453
457
|
const provider = catalogEntryProviderName(entry);
|
|
454
458
|
if (!provider) continue;
|
|
455
459
|
const plan = plans.get(provider);
|
|
@@ -482,7 +486,10 @@ export function finalizeAutoReviewModelOverride(
|
|
|
482
486
|
sourceModels: readonly RawEntry[] = [],
|
|
483
487
|
config?: Pick<OcxConfig, "providers">,
|
|
484
488
|
): AutoReviewModelOverrideResult {
|
|
485
|
-
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
|
|
489
|
+
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
|
|
490
|
+
models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
|
|
491
|
+
sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
|
|
492
|
+
);
|
|
486
493
|
if (config && configHasProviderAutoReview(config)) {
|
|
487
494
|
return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
|
|
488
495
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isCodexControlPlaneModel } from "../control-plane-models";
|
|
2
|
+
import { withCodexControlPlaneRows } from "./control-plane";
|
|
1
3
|
import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
|
|
2
4
|
import { clearModelCache } from "../model-cache";
|
|
3
5
|
import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
|
|
@@ -194,7 +196,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
194
196
|
const comboPublicSlugs = new Set(goModels
|
|
195
197
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
196
198
|
.map(catalogModelSlug));
|
|
197
|
-
for (const slug of gptSlugs) {
|
|
199
|
+
for (const slug of gptSlugs.filter(slug => !isCodexControlPlaneModel(slug))) {
|
|
198
200
|
const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
|
|
199
201
|
if (rank.has(slug)) native.priority = rank.get(slug)!;
|
|
200
202
|
nativeEntries.push(native);
|
|
@@ -224,7 +226,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
224
226
|
const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
|
|
225
227
|
?? accountNativeSlugs
|
|
226
228
|
?? gptSlugs;
|
|
227
|
-
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
|
|
229
|
+
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL && !isCodexControlPlaneModel(slug)).map(slug => (
|
|
228
230
|
nativeEntriesBySlug.get(slug)
|
|
229
231
|
?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
|
|
230
232
|
));
|
|
@@ -306,7 +308,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
306
308
|
delete entry.prefer_websockets;
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
|
-
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
|
|
311
|
+
return applyMultiAgentMode(withCodexControlPlaneRows(out, template ? [template] : [], true, wsEnabled), multiAgentMode, multiAgentV2Enabled, {
|
|
310
312
|
keepNativeChatGptOnV1,
|
|
311
313
|
preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
|
|
312
314
|
});
|
|
@@ -409,12 +411,12 @@ export function mergeCatalogModelsWithNativeRecovery(
|
|
|
409
411
|
): RawEntry[] {
|
|
410
412
|
const merged = [...primaryCatalogModels];
|
|
411
413
|
const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
|
|
412
|
-
const slug = recoverableNativeSlug(entry);
|
|
414
|
+
const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
|
|
413
415
|
return slug === null ? [] : [slug];
|
|
414
416
|
}));
|
|
415
417
|
for (const source of nativeRecoverySources) {
|
|
416
418
|
for (const entry of source) {
|
|
417
|
-
const slug = recoverableNativeSlug(entry);
|
|
419
|
+
const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
|
|
418
420
|
if (slug === null || recoveredNativeSlugs.has(slug)) continue;
|
|
419
421
|
merged.push(structuredClone(entry) as RawEntry);
|
|
420
422
|
recoveredNativeSlugs.add(slug);
|
|
@@ -460,6 +462,7 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
|
|
|
460
462
|
if (!pickerOrder.some(slug => !slug.includes("/"))) return;
|
|
461
463
|
const rankOf = modelPickerRank(pickerOrder);
|
|
462
464
|
for (const entry of entries) {
|
|
465
|
+
if (isCodexControlPlaneModel(entry.slug)) continue;
|
|
463
466
|
const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
|
|
464
467
|
entry[SPAWN_PRIORITY_FIELD] = natural;
|
|
465
468
|
entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
|
|
@@ -539,10 +542,12 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
539
542
|
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
540
543
|
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
541
544
|
const detachedCatalogModels = catalogModels
|
|
545
|
+
.filter(entry => !isCodexControlPlaneModel(entry.slug))
|
|
542
546
|
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
543
547
|
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
548
|
+
.filter(entry => !isCodexControlPlaneModel(entry.slug))
|
|
544
549
|
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
545
|
-
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
550
|
+
const detachedRoutedEntries = routedEntries.filter(entry => !isCodexControlPlaneModel(entry.slug)).map(entry => structuredClone(entry) as RawEntry);
|
|
546
551
|
// Track this invocation's generated custom rows, not ownership markers read from disk.
|
|
547
552
|
// Their builder already finalized exact native ladders and ordinary routed mock tiers.
|
|
548
553
|
const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
|
|
@@ -921,7 +926,10 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
921
926
|
// clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
|
|
922
927
|
// only their generated account row.
|
|
923
928
|
const versionedEntries = applyMultiAgentMode(
|
|
924
|
-
|
|
929
|
+
withCodexControlPlaneRows(
|
|
930
|
+
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
931
|
+
[...catalogModels, ...baselineCatalogModels, ...routedEntries], includeNativeOpenAi, wsEnabled,
|
|
932
|
+
),
|
|
925
933
|
multiAgentMode,
|
|
926
934
|
multiAgentV2Enabled,
|
|
927
935
|
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CODEX_INTERNAL_OPENAI_MODELS, isCodexControlPlaneModel } from "../control-plane-models";
|
|
2
|
+
import { COMBO_NAMESPACE } from "../../combos";
|
|
3
|
+
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
4
|
+
import { NATIVE_RESERVE_MODEL, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
|
|
5
|
+
import { isReserveCatalogProjection } from "./reserve";
|
|
6
|
+
import { pinnedNativeModelRows } from "./pinned-models";
|
|
7
|
+
import type { RawEntry } from "./parsing";
|
|
8
|
+
|
|
9
|
+
/** Hidden ordinary natives qualify; Reserve, account choices and routed aliases do not. */
|
|
10
|
+
export function hasOrdinaryNativeOpenAiRow(entries: readonly RawEntry[]): boolean {
|
|
11
|
+
return entries.some(entry => typeof entry.slug === "string"
|
|
12
|
+
&& !entry.slug.includes("/")
|
|
13
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(entry.slug)
|
|
14
|
+
&& entry.slug !== NATIVE_RESERVE_MODEL
|
|
15
|
+
&& !isReserveCatalogProjection(entry)
|
|
16
|
+
&& entry.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
17
|
+
&& entry.owned_by !== COMBO_NAMESPACE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Keep internal reviewer metadata separate from native synthesis, picker ordering and account clones. */
|
|
21
|
+
export function withCodexControlPlaneRows(
|
|
22
|
+
entries: RawEntry[],
|
|
23
|
+
sourceRows: readonly RawEntry[],
|
|
24
|
+
includeNativeOpenAi: boolean,
|
|
25
|
+
wsEnabled: boolean,
|
|
26
|
+
): RawEntry[] {
|
|
27
|
+
const models = entries.filter(entry => !isCodexControlPlaneModel(entry.slug));
|
|
28
|
+
if (!includeNativeOpenAi || !hasOrdinaryNativeOpenAiRow(models)) return models;
|
|
29
|
+
for (const slug of CODEX_INTERNAL_OPENAI_MODELS) {
|
|
30
|
+
const source = sourceRows.find(entry => entry.slug === slug)
|
|
31
|
+
?? pinnedNativeModelRows().find(entry => entry.slug === slug);
|
|
32
|
+
// A pin without the row degrades to Codex's own task-model fallback rather than failing the
|
|
33
|
+
// whole catalog write.
|
|
34
|
+
if (!source) continue;
|
|
35
|
+
const row: RawEntry = structuredClone(source);
|
|
36
|
+
row.visibility = "hide";
|
|
37
|
+
if (wsEnabled) row.supports_websockets = true;
|
|
38
|
+
else {
|
|
39
|
+
delete row.supports_websockets;
|
|
40
|
+
delete row.prefer_websockets;
|
|
41
|
+
}
|
|
42
|
+
models.push(row);
|
|
43
|
+
}
|
|
44
|
+
return models;
|
|
45
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasOrdinaryNativeOpenAiRow } from "./control-plane";
|
|
2
|
+
import { isCodexControlPlaneModel } from "../control-plane-models";
|
|
1
3
|
import { execFileSync } from "node:child_process";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -385,7 +387,7 @@ export function clampEntryToCodexSupportedEfforts(
|
|
|
385
387
|
entry: RawEntry,
|
|
386
388
|
supported: ReadonlySet<string> | null,
|
|
387
389
|
): void {
|
|
388
|
-
if (!supported) return;
|
|
390
|
+
if (!supported || isCodexControlPlaneModel(entry.slug)) return;
|
|
389
391
|
const levels = Array.isArray(entry.supported_reasoning_levels)
|
|
390
392
|
? entry.supported_reasoning_levels as Array<{ effort?: string }>
|
|
391
393
|
: null;
|
|
@@ -532,6 +534,14 @@ export function clampCatalogModelsToObservedCodexSupport(
|
|
|
532
534
|
else index += 1;
|
|
533
535
|
}
|
|
534
536
|
|
|
537
|
+
// Only Reserve rows are currently omitted above; ordinary natives keep a fallback ladder.
|
|
538
|
+
// Also repair a persisted orphan so a removed Reserve can never leave a reviewer behind.
|
|
539
|
+
if (!hasOrdinaryNativeOpenAiRow(models)) {
|
|
540
|
+
for (let index = models.length - 1; index >= 0; index -= 1) {
|
|
541
|
+
if (isCodexControlPlaneModel(models[index]!.slug)) models.splice(index, 1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
535
545
|
return {
|
|
536
546
|
removedEfforts: [...removed].sort(),
|
|
537
547
|
affectedModels: affected,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type OAuthActiveTokenObservation,
|
|
33
33
|
} from "../../oauth";
|
|
34
34
|
import { getAccountSet } from "../../oauth/store";
|
|
35
|
+
import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
|
|
35
36
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
36
37
|
import { modelInList } from "../../types";
|
|
37
38
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
@@ -389,15 +390,15 @@ export function captureProviderGather(
|
|
|
389
390
|
? authResolver.resolve(name, provider)
|
|
390
391
|
: undefined;
|
|
391
392
|
// A refreshing capture carries the stored origin so accounts on different hosts keep separate
|
|
392
|
-
// flights.
|
|
393
|
+
// flights. Host-scoped auth rebuilds its request; B retains its configured captured target. Observed capture never
|
|
393
394
|
// reads the live store.
|
|
394
395
|
const oauthApiBaseUrl = observedAuth
|
|
395
396
|
? observedAuth.oauthApiBaseUrl
|
|
396
|
-
: authResolver.kind === "refreshing" && provider.authMode === "oauth"
|
|
397
|
+
: authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
|
|
397
398
|
? getOAuthCredentialApiBaseUrl(name)
|
|
398
399
|
: undefined;
|
|
399
400
|
const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
|
|
400
|
-
const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth"
|
|
401
|
+
const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
|
|
401
402
|
? getAccountSet(name)?.activeAccountId
|
|
402
403
|
: undefined;
|
|
403
404
|
const resolved = resolveProviderModelDiscovery(name, provider);
|
|
@@ -531,6 +532,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
531
532
|
live: prov.liveModels ?? null,
|
|
532
533
|
base: prov.baseUrl ?? "",
|
|
533
534
|
adapter: prov.adapter ?? "",
|
|
535
|
+
instance: prov.anthropicOAuthInstance ?? null,
|
|
534
536
|
models: [...(prov.models ?? [])].sort(),
|
|
535
537
|
retain: [...(prov.retainModels ?? [])].sort(),
|
|
536
538
|
selected: [...(prov.selectedModels ?? [])].sort(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCodexControlPlaneModel } from "../control-plane-models";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -700,7 +701,7 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
|
|
|
700
701
|
&& entry.use_responses_lite === true
|
|
701
702
|
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
|
|
702
703
|
) return true;
|
|
703
|
-
if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
|
|
704
|
+
if (isCodexControlPlaneModel(slug) || UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
|
|
704
705
|
return false;
|
|
705
706
|
}
|
|
706
707
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAnthropicOAuthInstance } from "../../providers/anthropic-instance";
|
|
1
2
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
3
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
3
4
|
import { execFileSync } from "node:child_process";
|
|
@@ -28,11 +29,12 @@ import {
|
|
|
28
29
|
} from "../model-cache";
|
|
29
30
|
import {
|
|
30
31
|
buildModelsRequest,
|
|
31
|
-
|
|
32
|
+
getModelsOAuthAccessSnapshot,
|
|
32
33
|
observeActiveOAuthAccessToken,
|
|
33
34
|
resolveModelsAuthToken,
|
|
34
35
|
type OAuthActiveTokenObservation,
|
|
35
36
|
} from "../../oauth";
|
|
37
|
+
import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
|
|
36
38
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
37
39
|
import { modelInList } from "../../types";
|
|
38
40
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
@@ -145,7 +147,8 @@ export function observedModelsAuthResolver(
|
|
|
145
147
|
return { apiKey: resolveProviderApiKey(provider.apiKey), observed: true };
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
const observation =
|
|
150
|
+
const observation: OAuthActiveTokenObservation = mayResolveModelsOAuth(name, provider)
|
|
151
|
+
? observeActiveOAuthAccessToken(name, authStoreBuffer) : { kind: "missing" };
|
|
149
152
|
outcomes.push({ provider: name, state: observation.kind });
|
|
150
153
|
if (observation.kind !== "available") return { apiKey: undefined, observed: true };
|
|
151
154
|
return {
|
|
@@ -179,7 +182,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
179
182
|
// generation, so a request started with the former account cannot later publish its result.
|
|
180
183
|
const cacheGeneration = captureModelCacheGeneration(name);
|
|
181
184
|
const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
|
|
182
|
-
const anthropicSelection = name
|
|
185
|
+
const anthropicSelection = isAnthropicOAuthInstance(name) && prov.authMode === "oauth" && mayResolveModelsOAuth(name, prov)
|
|
183
186
|
? captureOAuthAccountSelection(name) : null;
|
|
184
187
|
if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
|
|
185
188
|
const seedVertexDefault = prov.adapter === "google"
|
|
@@ -257,9 +260,11 @@ export async function fetchProviderModelsWithAuth(
|
|
|
257
260
|
}
|
|
258
261
|
return observed(configured, "authoritative");
|
|
259
262
|
}
|
|
260
|
-
const auth: ModelsAuthResolution =
|
|
263
|
+
const auth: ModelsAuthResolution = prov.authMode === "oauth" && !mayResolveModelsOAuth(name, prov)
|
|
264
|
+
? { apiKey: undefined, observed: resolveAuth.kind === "observed" }
|
|
265
|
+
: captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
261
266
|
? prov.authMode === "oauth"
|
|
262
|
-
? await
|
|
267
|
+
? await getModelsOAuthAccessSnapshot(name, prov)
|
|
263
268
|
.then(snapshot => ({
|
|
264
269
|
apiKey: snapshot.accessToken,
|
|
265
270
|
observed: false,
|
|
@@ -274,7 +279,8 @@ export async function fetchProviderModelsWithAuth(
|
|
|
274
279
|
: resolveAuth.resolve(name, prov));
|
|
275
280
|
const apiKey = auth.apiKey;
|
|
276
281
|
const maySendAnthropicDiscovery = () => {
|
|
277
|
-
if (name
|
|
282
|
+
if (!isAnthropicOAuthInstance(name) || prov.authMode !== "oauth") return true;
|
|
283
|
+
if (!mayResolveModelsOAuth(name, prov)) return false;
|
|
278
284
|
const selected = captureOAuthAccountSelection(name);
|
|
279
285
|
const row = auth.oauthAccountId ? getAccountCredentialWithStatus(name, auth.oauthAccountId) : null;
|
|
280
286
|
return !!anthropicSelection && !!selected && !!row && !row.paused && !row.needsReauth
|
|
@@ -633,7 +639,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
633
639
|
}
|
|
634
640
|
// The captured request predates any refresh, so a refreshing gather rebuilds it
|
|
635
641
|
// from the auth it resolved: the token and its origin, together.
|
|
636
|
-
const request = resolveAuth.kind === "refreshing"
|
|
642
|
+
const request = resolveAuth.kind === "refreshing" && name !== "anthropic2"
|
|
637
643
|
? captureModelsRequest(name, prov, auth.oauthApiBaseUrl)
|
|
638
644
|
: captured.request;
|
|
639
645
|
const url = request.url;
|
|
@@ -653,7 +659,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
653
659
|
const failedDiscoveryFallback = (
|
|
654
660
|
failure: ProviderModelDiscoveryFailure,
|
|
655
661
|
): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
|
|
656
|
-
if (!isCurrentCacheGeneration()) {
|
|
662
|
+
if (!isCurrentCacheGeneration() || !maySendAnthropicDiscovery()) {
|
|
657
663
|
return {
|
|
658
664
|
models: withConfiguredRetention(failedDiscoveryConfigured),
|
|
659
665
|
fallback: "configured",
|
|
@@ -854,7 +860,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
854
860
|
`[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
|
|
855
861
|
);
|
|
856
862
|
}
|
|
857
|
-
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
863
|
+
if (!maySendAnthropicDiscovery() || !setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
858
864
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
859
865
|
}
|
|
860
866
|
markProviderDiscoveryOk(name, liveModelCount);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Exact Codex control-plane ids routed to canonical OpenAI, outside the public native roster. */
|
|
2
|
+
export const CODEX_INTERNAL_OPENAI_MODELS: ReadonlySet<string> = new Set(["codex-auto-review"]);
|
|
3
|
+
|
|
4
|
+
export function isCodexControlPlaneModel(slug: unknown): boolean {
|
|
5
|
+
return typeof slug === "string" && CODEX_INTERNAL_OPENAI_MODELS.has(slug);
|
|
6
|
+
}
|