@bitkyc08/opencodex 2.79.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/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- 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 +10 -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/autostart-health.ts +3 -3
- 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/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- 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 +347 -199
- 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 +16 -16
- 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-scope.ts +4 -1
- 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/combo-requested-effort.ts +14 -0
- 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 +21 -18
- 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/desktop-startup.ts +100 -3
- 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/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
24
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
24
|
+
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
25
25
|
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
|
|
26
26
|
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
|
|
27
27
|
import { canonicalizeReasoningEfforts } from "../reasoning-effort";
|
|
@@ -69,6 +69,18 @@ import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfig
|
|
|
69
69
|
export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo";
|
|
70
70
|
export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo";
|
|
71
71
|
import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
|
|
72
|
+
import {
|
|
73
|
+
buildCommandCodeClientConfig,
|
|
74
|
+
summarizeCommandCode,
|
|
75
|
+
buildCommandCodeContribution,
|
|
76
|
+
commandCodeProviderRoot,
|
|
77
|
+
type CommandCodeGeneratedConfig,
|
|
78
|
+
type CommandCodeModelEntry,
|
|
79
|
+
type CommandCodeProviderBlock,
|
|
80
|
+
} from "./config-export/commandcode";
|
|
81
|
+
|
|
82
|
+
export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
|
|
83
|
+
export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
|
|
72
84
|
|
|
73
85
|
|
|
74
86
|
|
|
@@ -474,6 +486,43 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
474
486
|
return join(zcodeHomeDir(env, home), "v2", "config.json");
|
|
475
487
|
}
|
|
476
488
|
|
|
489
|
+
/**
|
|
490
|
+
* The home directory Command Code itself resolves: `env.HOME ?? env.USERPROFILE`
|
|
491
|
+
* (`homeDir15` in the published `command-code@1.66.0` bundle).
|
|
492
|
+
*
|
|
493
|
+
* On POSIX `os.homedir()` already reads `$HOME`, so the two agree and `home` is
|
|
494
|
+
* returned as is. On Windows `os.homedir()` reads `%USERPROFILE%` and ignores
|
|
495
|
+
* `HOME`, which Git for Windows, MSYS2 and Cygwin set, so a user whose `HOME` differs
|
|
496
|
+
* would get a provider block in a store Command Code never opens. Only the OS-default
|
|
497
|
+
* home is replaced: a caller that injects a different root (a test, the writer's
|
|
498
|
+
* `input.home`) keeps it, and a blank or relative value falls back to `home`.
|
|
499
|
+
*/
|
|
500
|
+
export function commandCodeUserHome(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
501
|
+
if (platform !== "win32" || home !== homedir()) return home;
|
|
502
|
+
const value = env.HOME ?? env.USERPROFILE;
|
|
503
|
+
return typeof value === "string" && value.trim() && win32.isAbsolute(value) ? value : home;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Command Code's provider store directory — `.commandcode` under the home the client
|
|
508
|
+
* resolves (see {@link commandCodeUserHome}), with no override.
|
|
509
|
+
*
|
|
510
|
+
* There is deliberately no `COMMANDCODE_HOME` here, unlike the other clients in this
|
|
511
|
+
* file: `grep -c COMMANDCODE_HOME` over the shipped `dist/cli.mjs` and
|
|
512
|
+
* `dist/index.mjs` returns 0. An override we honour but the client ignores is worse
|
|
513
|
+
* than none: `enable` would report success and write a provider block at a path no
|
|
514
|
+
* Command Code process ever opens, and the user would see an empty model list with no
|
|
515
|
+
* error anywhere. An apply against a relocated home is instead refused by `detectDir`
|
|
516
|
+
* not existing.
|
|
517
|
+
*/
|
|
518
|
+
export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
519
|
+
return join(commandCodeUserHome(env, home, platform), ".commandcode");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
|
|
523
|
+
return join(commandCodeHomeDir(env, home, platform), "providers.json");
|
|
524
|
+
}
|
|
525
|
+
|
|
477
526
|
/**
|
|
478
527
|
* The provider store a current ZCode reads, which is NOT the file above.
|
|
479
528
|
*
|
|
@@ -1487,6 +1536,20 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1487
1536
|
// remote binds refuse — same reasoning as MCode.
|
|
1488
1537
|
loopbackOnly: true,
|
|
1489
1538
|
},
|
|
1539
|
+
commandcode: {
|
|
1540
|
+
id: "commandcode",
|
|
1541
|
+
filename: "providers.json",
|
|
1542
|
+
destination: env => commandCodeConfigPath(env),
|
|
1543
|
+
// No env var exists behind this integration: buildCommandCodeClientConfig
|
|
1544
|
+
// writes `apiKey: false`, the documented keyless form for a loopback endpoint.
|
|
1545
|
+
apiKeyEnv: "",
|
|
1546
|
+
exportHint: "The provider is written with `apiKey: false` — Command Code treats a keyless loopback endpoint as already authenticated. No key is stored or referenced.",
|
|
1547
|
+
build: buildCommandCodeClientConfig,
|
|
1548
|
+
format: "json",
|
|
1549
|
+
summarize: summarizeCommandCode,
|
|
1550
|
+
buildContribution: buildCommandCodeContribution,
|
|
1551
|
+
loopbackOnly: true,
|
|
1552
|
+
},
|
|
1490
1553
|
prime: {
|
|
1491
1554
|
id: "prime",
|
|
1492
1555
|
filename: "prime-models.json",
|
|
@@ -252,6 +252,13 @@ export function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
export interface CodexAuthAccountsListOptions {
|
|
256
|
+
/** Finish deferred registration; only a dashboard session carries that inference consent. */
|
|
257
|
+
validatePending?: boolean;
|
|
258
|
+
/** An operator's explicit refresh command, which may retry a grant already recorded as dead. */
|
|
259
|
+
explicitRefresh?: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
255
262
|
export interface CodexAuthAccountsSnapshot {
|
|
256
263
|
accounts: CodexAuthAccountDto[];
|
|
257
264
|
mainIdentityGeneration: number;
|
|
@@ -260,7 +267,7 @@ export interface CodexAuthAccountsSnapshot {
|
|
|
260
267
|
export async function listCodexAuthAccountsSnapshot(
|
|
261
268
|
config: OcxConfig,
|
|
262
269
|
forceRefresh = false,
|
|
263
|
-
options:
|
|
270
|
+
options: CodexAuthAccountsListOptions = {},
|
|
264
271
|
): Promise<CodexAuthAccountsSnapshot> {
|
|
265
272
|
const runtimeConfig = getRuntimeConfig(config);
|
|
266
273
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
@@ -281,7 +288,8 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
281
288
|
// identity when the switch is on and nothing has been observed yet.
|
|
282
289
|
const creditsIdentity = runtimeConfig.showCodexCredits === true ? poolQuotaHistoryIdentity(account.id) ?? null : null;
|
|
283
290
|
const creditsUnobserved = creditsIdentity !== null && !hasCodexCreditsObservation(account.id, creditsIdentity);
|
|
284
|
-
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
|
|
291
|
+
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
|
|
292
|
+
options.validatePending === true, undefined, undefined, options.explicitRefresh === true);
|
|
285
293
|
} catch (error) {
|
|
286
294
|
if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
|
|
287
295
|
quotaResult = {
|
|
@@ -447,7 +455,7 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
447
455
|
export async function listCodexAuthAccounts(
|
|
448
456
|
config: OcxConfig,
|
|
449
457
|
forceRefresh = false,
|
|
450
|
-
options:
|
|
458
|
+
options: CodexAuthAccountsListOptions = {},
|
|
451
459
|
): Promise<CodexAuthAccountDto[]> {
|
|
452
460
|
return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
|
|
453
461
|
}
|
|
@@ -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
|
|
|
@@ -4,7 +4,7 @@ import type { OcxConfig } from "../types";
|
|
|
4
4
|
import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
|
|
5
5
|
import { collectRoutingAdoption, type RoutingAdoptionEvidence } from "./routing-adoption";
|
|
6
6
|
import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
|
|
7
|
-
import {
|
|
7
|
+
import { diagnoseDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
|
|
8
8
|
|
|
9
9
|
export type StartupProtection = "service" | "desktop" | "shim" | "none";
|
|
10
10
|
export type StartupHealthStatus = "native" | "protected" | "at-risk";
|
|
@@ -90,7 +90,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
90
90
|
// We can only credit an opencodex service/shim for routing that opencodex owns.
|
|
91
91
|
// An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
|
|
92
92
|
const ownsLocalRouting = inputs.routingKind === "opencodex-local";
|
|
93
|
-
const desktopEffective = inputs.platform === "darwin" && !inputs.diagnosticStale
|
|
93
|
+
const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
|
|
94
94
|
&& inputs.desktop?.owned === true && inputs.desktop.loginEnabled
|
|
95
95
|
&& inputs.desktop.running && inputs.desktop.viable;
|
|
96
96
|
const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
|
|
@@ -147,7 +147,7 @@ export function collectStartupHealth(
|
|
|
147
147
|
const shim = diagnostics.shim ?? diagnoseCodexShim();
|
|
148
148
|
const service = diagnostics.service ?? diagnoseService();
|
|
149
149
|
const routingKind = diagnostics.routingKind ?? getCodexRoutingKind();
|
|
150
|
-
const desktop = diagnostics.desktop ??
|
|
150
|
+
const desktop = diagnostics.desktop ?? diagnoseDesktopStartup();
|
|
151
151
|
const routingAdoption = diagnostics.routingAdoption
|
|
152
152
|
?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
|
|
153
153
|
return deriveStartupHealth({
|
|
@@ -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
|
+
}
|