@bitkyc08/opencodex 2.88.0 → 2.90.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 +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -69,7 +69,7 @@ import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
|
69
69
|
import { maskAccountId } from "../lib/privacy";
|
|
70
70
|
import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
|
|
71
71
|
import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
|
|
72
|
-
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
72
|
+
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, isValidWhamHistoryObservation, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
|
|
73
73
|
import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
|
|
74
74
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
75
75
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
@@ -97,6 +97,7 @@ import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
|
97
97
|
import { getMainReserveAuthorization, isMainReserveAuthorizationLive, nativeUserIdClaims, type MainReserveAuthorization } from "./reserve-availability";
|
|
98
98
|
import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
99
99
|
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
100
|
+
import { CODEX_MAIN_DUPLICATE_ACTION, configuredCodexMainDuplicate } from "./collision-identity";
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
103
|
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
|
|
@@ -324,6 +325,8 @@ export type CodexAuthContext =
|
|
|
324
325
|
writerGeneration: number;
|
|
325
326
|
/** Captured before async credential work; never reconstructed after the upstream response. */
|
|
326
327
|
mainQuotaWriter?: MainQuotaWriter;
|
|
328
|
+
/** Immutable credential proof captured while materializing this physical attempt's headers. */
|
|
329
|
+
mainQuotaDispatch?: MainQuotaDispatch;
|
|
327
330
|
reserveAuthorization?: MainReserveAuthorization;
|
|
328
331
|
accessToken: string;
|
|
329
332
|
chatgptAccountId: string;
|
|
@@ -665,6 +668,7 @@ function assertReserveAdmission(config: CodexAuthPolicyConfig): void {
|
|
|
665
668
|
}
|
|
666
669
|
}
|
|
667
670
|
|
|
671
|
+
/** Authorize a reserve request for the main credential, re-checking admission and writer liveness, and publish the ordinary quota observed during the capability read. */
|
|
668
672
|
async function authorizeReserveCredential(
|
|
669
673
|
token: { accessToken: string; chatgptAccountId: string },
|
|
670
674
|
writer: MainQuotaWriter | undefined,
|
|
@@ -681,9 +685,11 @@ async function authorizeReserveCredential(
|
|
|
681
685
|
const authorization = isMainReserveAuthorizationLive(existing, token) ? existing
|
|
682
686
|
: await getMainReserveAuthorization({
|
|
683
687
|
token, writer, signal,
|
|
688
|
+
/** Publish the ordinary usage observed during the reserve capability read: parse it once and reuse that observation as the advisory decision evidence when it is a valid history observation. */
|
|
684
689
|
observeOrdinaryQuota(data, capturedWriter) {
|
|
685
|
-
|
|
686
|
-
|
|
690
|
+
const parsed = parseUsageQuota(data);
|
|
691
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, parsed, writerGeneration,
|
|
692
|
+
capturedWriter, parseMainPolicyUsageQuota(data), undefined, false, false, isValidWhamHistoryObservation(data) ? parsed : null);
|
|
687
693
|
},
|
|
688
694
|
});
|
|
689
695
|
// The capability read also publishes ordinary quota. A new 99% reading or cooldown wins.
|
|
@@ -769,8 +775,10 @@ export function createCodexAuthDispatchGuard(
|
|
|
769
775
|
terminalHelper = false,
|
|
770
776
|
): ((headers: Headers) => void) | undefined {
|
|
771
777
|
const reserveGuard = createCodexReserveDispatchGuard(ctx, config, modelId, admission, terminalHelper);
|
|
772
|
-
if (ctx.kind !== "pool") return reserveGuard;
|
|
778
|
+
if (ctx.kind !== "pool" && ctx.kind !== "main-pool") return reserveGuard;
|
|
773
779
|
return headers => {
|
|
780
|
+
if (config.pausedCodexAccountIds?.includes(ctx.accountId)) throw new CodexPoolAuthenticationError("Selected Codex account is paused; resume it in account settings");
|
|
781
|
+
if (ctx.kind === "main-pool") { reserveGuard?.(headers); return; }
|
|
774
782
|
assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx, config));
|
|
775
783
|
reserveGuard?.(headers);
|
|
776
784
|
};
|
|
@@ -1075,6 +1083,33 @@ async function awaitMainAccountPolicyBindingSettled(
|
|
|
1075
1083
|
if (!settled) throw new CodexMainProfileDrainingError();
|
|
1076
1084
|
}
|
|
1077
1085
|
|
|
1086
|
+
/** Memory-only ownership check shared by Direct admission and pre-auth hard-lock routing. */
|
|
1087
|
+
export async function mainAccountHardLockAppliesToCaller(
|
|
1088
|
+
headers: Headers,
|
|
1089
|
+
policy: CodexAuthPolicyConfig,
|
|
1090
|
+
options: Pick<ResolveCodexAuthContextOptions,
|
|
1091
|
+
"substituteMainCredentialForDirect" | "signal" | "mainAccountPolicyBindingWaitMs"> = {},
|
|
1092
|
+
): Promise<boolean> {
|
|
1093
|
+
if (options.substituteMainCredentialForDirect === true) return true;
|
|
1094
|
+
if (!hasCallerCodexBearer(headers)) return false;
|
|
1095
|
+
if (isMainAccountHardLockEnabled(policy)) await awaitMainAccountPolicyBindingSettled(options);
|
|
1096
|
+
return callerMatchesObservedMain(headers);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Resolve ownership once, then hand back a synchronous proof. Main identity or its observed bearer
|
|
1101
|
+
* can change across the await; the hard-lock reroute rechecks the proof with no await between it
|
|
1102
|
+
* and the body rewrite, so a replacement main's lock never reroutes the original caller.
|
|
1103
|
+
*/
|
|
1104
|
+
export async function mainAccountHardLockCallerProof(
|
|
1105
|
+
headers: Headers,
|
|
1106
|
+
policy: CodexAuthPolicyConfig,
|
|
1107
|
+
options: Parameters<typeof mainAccountHardLockAppliesToCaller>[2] = {},
|
|
1108
|
+
): Promise<(() => boolean) | null> {
|
|
1109
|
+
if (!await mainAccountHardLockAppliesToCaller(headers, policy, options)) return null;
|
|
1110
|
+
return options.substituteMainCredentialForDirect === true ? () => true : () => callerMatchesObservedMain(headers);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1078
1113
|
/**
|
|
1079
1114
|
* Resolve the credential for one Codex turn. A main hard-lock reached through unpinned pool
|
|
1080
1115
|
* selection is annotated with the earliest known pool recovery, so the refusal can name a retry
|
|
@@ -1167,8 +1202,7 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1167
1202
|
// Trusted substitution still has to claim and validate stored main below.
|
|
1168
1203
|
if (!substituteStoredMain && !hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
1169
1204
|
if (!substituteStoredMain) {
|
|
1170
|
-
if (
|
|
1171
|
-
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
1205
|
+
if (await mainAccountHardLockAppliesToCaller(headers, policy, options)) assertMainAccountPolicy(policy);
|
|
1172
1206
|
assertCallerOwnedMainPoolNotCooled();
|
|
1173
1207
|
if (reserve) {
|
|
1174
1208
|
const selected = materializeCodexUpstreamAuth(headers, { kind: "main", accountId: null }, { config: policy });
|
|
@@ -1417,7 +1451,9 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1417
1451
|
// cool down. Hand back any trial first -- no Pool account is being sent to.
|
|
1418
1452
|
if (selected === MAIN_CODEX_ACCOUNT_ID && requestScopedMainCredential) {
|
|
1419
1453
|
releaseTransientProbeGrant();
|
|
1420
|
-
|
|
1454
|
+
const context = await resolveCallerOwnedMainContext();
|
|
1455
|
+
console.info("[codex] Pool selected caller-owned main credential.");
|
|
1456
|
+
return context;
|
|
1421
1457
|
}
|
|
1422
1458
|
if (!selected) {
|
|
1423
1459
|
// A retry that excluded a failed Pool account may still use the validated caller-owned
|
|
@@ -1427,9 +1463,12 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1427
1463
|
if (
|
|
1428
1464
|
requestScopedMainCredential
|
|
1429
1465
|
&& fixedAccountId === undefined
|
|
1466
|
+
&& !(callerMatchesObservedMain(headers) && policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID))
|
|
1430
1467
|
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
1431
1468
|
) {
|
|
1432
|
-
|
|
1469
|
+
const context = await resolveCallerOwnedMainContext();
|
|
1470
|
+
console.info("[codex] Pool used caller-owned main credential: no selectable stored account.");
|
|
1471
|
+
return context;
|
|
1433
1472
|
}
|
|
1434
1473
|
if (fixedAccountId !== undefined) {
|
|
1435
1474
|
if (modelEligibleAccountIds && !modelEligibleAccountIds.has(fixedAccountId)) {
|
|
@@ -1528,6 +1567,7 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1528
1567
|
// deferred credential must never become request auth through that fallback.
|
|
1529
1568
|
try {
|
|
1530
1569
|
assertCodexAccountValidationReady(accountId);
|
|
1570
|
+
if (configuredCodexMainDuplicate(config, accountId)) throw new CodexPoolAuthenticationError(CODEX_MAIN_DUPLICATE_ACTION);
|
|
1531
1571
|
assertPoolAccountCredits(accountId, poolCreditHoldResetAt(policy, accountId, config));
|
|
1532
1572
|
} catch (cause) {
|
|
1533
1573
|
// Nothing will reach upstream, so give the trial back instead of leaving the held account
|
|
@@ -1585,9 +1625,12 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1585
1625
|
// alternate is eligible. A validated caller may still serve this request,
|
|
1586
1626
|
// just as it can after an upstream rejection, without changing Pool state.
|
|
1587
1627
|
if (requestScopedMainCredential && fixedAccountId === undefined
|
|
1628
|
+
&& !(callerMatchesObservedMain(headers) && policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID))
|
|
1588
1629
|
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
1589
1630
|
&& !callerIsCooledPoolAccount(headers, config, accountId)) {
|
|
1590
|
-
|
|
1631
|
+
const context = await resolveCallerOwnedMainContext();
|
|
1632
|
+
console.info("[codex] Pool used caller-owned main credential: stored account cooldown.");
|
|
1633
|
+
return context;
|
|
1591
1634
|
}
|
|
1592
1635
|
throw new CodexAccountCooldownError(accountId, cooldownUntil, cooldown?.cooldownSource, cooldown?.quotaScope);
|
|
1593
1636
|
}
|
|
@@ -1655,6 +1698,7 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1655
1698
|
try {
|
|
1656
1699
|
const token = await getValidCodexToken(accountId, { signal: options.signal });
|
|
1657
1700
|
assertCodexAccountValidationReady(accountId);
|
|
1701
|
+
if (configuredCodexMainDuplicate(config, accountId)) throw new CodexPoolAuthenticationError(CODEX_MAIN_DUPLICATE_ACTION);
|
|
1658
1702
|
assertPoolAccountCredits(accountId, poolCreditHoldResetAt(policy, accountId, config));
|
|
1659
1703
|
return bindPoolCreditPolicy({
|
|
1660
1704
|
kind: "pool",
|
|
@@ -1676,7 +1720,8 @@ async function resolveCodexAuthContextUnannotated(
|
|
|
1676
1720
|
releaseTransientProbeGrant();
|
|
1677
1721
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
1678
1722
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
1679
|
-
|
|
1723
|
+
// Post-refresh eligibility refusals do not invalidate a healthy credential.
|
|
1724
|
+
if (cause instanceof CodexPoolAccountCreditsOffError || cause instanceof CodexPoolAuthenticationError) throw cause;
|
|
1680
1725
|
if (!options.signal?.aborted && shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
1681
1726
|
markAccountNeedsReauth(accountId, writerGeneration);
|
|
1682
1727
|
}
|
|
@@ -1761,6 +1806,8 @@ export function materializeCodexUpstreamAuth(
|
|
|
1761
1806
|
if (ctx.kind === "main-pool") {
|
|
1762
1807
|
ctx.mainQuotaWriter = observeSelectedMainCredential(ctx, ctx.mainQuotaWriter);
|
|
1763
1808
|
assertMainAccountPolicy(options.config);
|
|
1809
|
+
ctx.mainQuotaDispatch = ctx.mainQuotaWriter && isMainQuotaWriterLive(ctx.mainQuotaWriter)
|
|
1810
|
+
? selectedMainQuotaDispatch(selected) : undefined;
|
|
1764
1811
|
}
|
|
1765
1812
|
assertMaterializedReserve(selected, ctx, options);
|
|
1766
1813
|
return selected;
|
|
@@ -195,6 +195,7 @@ export function deriveComboCatalogModel(
|
|
|
195
195
|
...(combo.alias ? { alias: combo.alias } : {}),
|
|
196
196
|
...(combo.nativeAlias ? { nativeAlias: true } : {}),
|
|
197
197
|
...(combo.displayName ? { displayName: combo.displayName } : {}),
|
|
198
|
+
...(combo.description ? { description: combo.description } : {}),
|
|
198
199
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
199
200
|
...(members.every(member => member.parallelToolCalls === true)
|
|
200
201
|
? { parallelToolCalls: true }
|
|
@@ -401,6 +402,20 @@ export function exactComboCatalogSlugs(
|
|
|
401
402
|
}));
|
|
402
403
|
}
|
|
403
404
|
|
|
405
|
+
/** Current combo row descriptions by public catalog slug, for retained-row re-projection (#7229). */
|
|
406
|
+
export function comboCatalogDescriptions(
|
|
407
|
+
config: Pick<OcxConfig, "combos" | "disabledModels">,
|
|
408
|
+
): Map<string, string> {
|
|
409
|
+
const current = exactComboCatalogSlugs(config);
|
|
410
|
+
return new Map(listComboIds(config).flatMap(id => {
|
|
411
|
+
const raw = config.combos?.[id];
|
|
412
|
+
const alias = typeof raw?.alias === "string" ? raw.alias.trim() : "";
|
|
413
|
+
const publicSlug = alias || comboModelId(id);
|
|
414
|
+
const description = typeof raw?.description === "string" ? raw.description.trim() : "";
|
|
415
|
+
return description && current.has(publicSlug) ? [[publicSlug, description] as const] : [];
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
|
|
404
419
|
export function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
405
420
|
const normalized = {
|
|
406
421
|
provider: model.provider,
|
|
@@ -5,6 +5,7 @@ import { encodeRoutedModelId } from "../../providers/slug-codec";
|
|
|
5
5
|
import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
|
|
6
6
|
import { readConfiguredAutoReviewModel } from "./parsing";
|
|
7
7
|
import type { RawEntry } from "./parsing";
|
|
8
|
+
import { hasOcxRoutedAuthorship } from "./ownership";
|
|
8
9
|
import { configuredCatalogEntry } from "./subagent-roster";
|
|
9
10
|
|
|
10
11
|
const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
|
|
@@ -38,7 +39,7 @@ function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
|
38
39
|
if (isCodexControlPlaneModel(entry.slug)) return false;
|
|
39
40
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
40
41
|
return slug.includes("/")
|
|
41
|
-
|| (
|
|
42
|
+
|| hasOcxRoutedAuthorship(entry);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
|
|
@@ -4,6 +4,7 @@ import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasonin
|
|
|
4
4
|
import { clearModelCache } from "../model-cache";
|
|
5
5
|
import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
|
|
6
6
|
import { COMBO_NAMESPACE } from "../../combos";
|
|
7
|
+
import { hasOcxComboAuthorship, hasOcxRoutedAuthorship, reapplyRetainedComboDescription, ROUTED_DESCRIPTION_PREFIX } from "./ownership";
|
|
7
8
|
import {
|
|
8
9
|
CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
9
10
|
CODEX_PROVIDER_MODEL_CATALOG_KIND,
|
|
@@ -218,7 +219,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
218
219
|
const routed = deriveEntry(
|
|
219
220
|
template,
|
|
220
221
|
slug,
|
|
221
|
-
|
|
222
|
+
`${ROUTED_DESCRIPTION_PREFIX}${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
|
|
222
223
|
5,
|
|
223
224
|
nativeAlias,
|
|
224
225
|
exactComboSlugs,
|
|
@@ -276,7 +277,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
276
277
|
const e = deriveEntry(
|
|
277
278
|
template,
|
|
278
279
|
slug,
|
|
279
|
-
|
|
280
|
+
`${ROUTED_DESCRIPTION_PREFIX}${m.provider} (${m.owned_by ?? m.provider}).`,
|
|
280
281
|
5,
|
|
281
282
|
m,
|
|
282
283
|
exactComboSlugs,
|
|
@@ -380,15 +381,15 @@ export function orderForModelPicker(
|
|
|
380
381
|
* True when an existing catalog row was authored by OpenCodex routing (#855).
|
|
381
382
|
* Every generated routed row — current full-slug form, the June–July 2026
|
|
382
383
|
* provider-name form, and legacy combo aliases — carries the stable
|
|
383
|
-
* description prefix `Routed via opencodex →
|
|
384
|
+
* description prefix `Routed via opencodex → `, or — when a combo overrides its description
|
|
385
|
+
* (#7229) — the `opencodex_routed_origin` marker (./ownership); foreign rows from Cursor or
|
|
384
386
|
* user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
|
|
385
387
|
* describes history compatibility; neither is an authorship signal.
|
|
386
388
|
*/
|
|
387
389
|
export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
388
390
|
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
389
|
-
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
390
391
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
391
|
-
return slug.includes("/") &&
|
|
392
|
+
return slug.includes("/") && hasOcxRoutedAuthorship(entry);
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
@@ -503,6 +504,11 @@ export interface ObservedCatalogMergeInput {
|
|
|
503
504
|
readonly multiAgentV2Enabled: boolean;
|
|
504
505
|
readonly keepNativeChatGptOnV1?: boolean;
|
|
505
506
|
readonly exactComboSlugs: ReadonlySet<string>;
|
|
507
|
+
/**
|
|
508
|
+
* Current combo row descriptions by public slug (#7229). When supplied, retained combo rows
|
|
509
|
+
* are re-projected onto it; absent means the caller has no config and leaves them unchanged.
|
|
510
|
+
*/
|
|
511
|
+
readonly comboDescriptions?: ReadonlyMap<string, string>;
|
|
506
512
|
readonly hasPhysicalComboProvider: boolean;
|
|
507
513
|
readonly includeNativeOpenAi: boolean;
|
|
508
514
|
readonly accountBoundEntries: readonly RawEntry[];
|
|
@@ -545,6 +551,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
545
551
|
multiAgentV2Enabled,
|
|
546
552
|
keepNativeChatGptOnV1,
|
|
547
553
|
exactComboSlugs,
|
|
554
|
+
comboDescriptions,
|
|
548
555
|
hasPhysicalComboProvider,
|
|
549
556
|
includeNativeOpenAi,
|
|
550
557
|
accountBoundEntries,
|
|
@@ -639,8 +646,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
639
646
|
}
|
|
640
647
|
const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
|
|
641
648
|
isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
642
|
-
&&
|
|
643
|
-
&& entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
|
|
649
|
+
&& hasOcxComboAuthorship(entry)
|
|
644
650
|
)));
|
|
645
651
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
646
652
|
const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
@@ -802,6 +808,10 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
802
808
|
delete entry[SPAWN_PRIORITY_FIELD];
|
|
803
809
|
}
|
|
804
810
|
const slug = String(entry.slug);
|
|
811
|
+
if (comboDescriptions !== undefined && exactComboSlugs.has(slug)
|
|
812
|
+
&& (isNativeAliasCatalogEntry(entry) || (entry.owned_by === COMBO_NAMESPACE && hasOcxComboAuthorship(entry)))) {
|
|
813
|
+
reapplyRetainedComboDescription(entry, comboDescriptions.get(slug));
|
|
814
|
+
}
|
|
805
815
|
if (!isOcxAuthoredRoutedEntry(entry)) continue;
|
|
806
816
|
// The builder no longer copies a template's comp_hash onto routed rows (#5796), but a row
|
|
807
817
|
// kept from disk may still carry one. Custom rows, Codex-forward aliases included, never
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isGpt56NativeSlug,
|
|
27
27
|
} from "./effort";
|
|
28
28
|
import { CATALOG_INACTIVE_REASON_FIELD, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
|
|
29
|
+
import { applyRoutedDescription } from "./ownership";
|
|
29
30
|
|
|
30
31
|
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
|
|
31
32
|
if (priority !== 9) clone.priority = priority;
|
|
@@ -128,7 +129,7 @@ export function deriveEntry(
|
|
|
128
129
|
delete e[SPAWN_PRIORITY_FIELD];
|
|
129
130
|
e.slug = slug;
|
|
130
131
|
e.display_name = routedDisplayName(slug, model);
|
|
131
|
-
e
|
|
132
|
+
applyRoutedDescription(e, desc, model);
|
|
132
133
|
e.priority = priority;
|
|
133
134
|
e.visibility = "list";
|
|
134
135
|
if ("upgrade" in e) e.upgrade = null;
|
|
@@ -212,6 +213,7 @@ export function deriveEntry(
|
|
|
212
213
|
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
213
214
|
: {}),
|
|
214
215
|
};
|
|
216
|
+
applyRoutedDescription(entry, desc, model);
|
|
215
217
|
if (isRouted) {
|
|
216
218
|
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
217
219
|
applyReasoningLevels(
|
|
@@ -38,6 +38,7 @@ import type { RawEntry } from "./parsing";
|
|
|
38
38
|
import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
|
|
39
39
|
import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
40
40
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
41
|
+
import { ROUTED_ORIGIN_FIELD } from "./ownership";
|
|
41
42
|
import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
|
|
42
43
|
import {
|
|
43
44
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
@@ -813,6 +814,7 @@ export function observedReserveCatalogSource(
|
|
|
813
814
|
}
|
|
814
815
|
source.slug = NATIVE_RESERVE_MODEL;
|
|
815
816
|
delete source.opencodex_catalog_kind;
|
|
817
|
+
delete source[ROUTED_ORIGIN_FIELD];
|
|
816
818
|
delete source[RESERVE_METADATA_SOURCE_FIELD];
|
|
817
819
|
delete source[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER];
|
|
818
820
|
delete source[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER];
|
|
@@ -189,7 +189,6 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
|
|
|
189
189
|
);
|
|
190
190
|
}
|
|
191
191
|
export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
|
|
192
|
-
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
193
192
|
// xAI /models advertises both the dated deployment and this floating alias.
|
|
194
193
|
// Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
|
|
195
194
|
if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
|
|
@@ -205,7 +204,6 @@ export function shouldRetainConfiguredProviderModel(
|
|
|
205
204
|
prov?: OcxProviderConfig,
|
|
206
205
|
): boolean {
|
|
207
206
|
if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
|
|
208
|
-
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
209
207
|
if (modelInList(prov?.retainModels, modelId)) return true;
|
|
210
208
|
return false;
|
|
211
209
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { COMBO_NAMESPACE } from "../../combos/identifiers";
|
|
2
|
+
import type { CatalogModel, RawEntry } from "./parsing";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authorship of OpenCodex-generated routed catalog rows (#855, #7229).
|
|
6
|
+
*
|
|
7
|
+
* A generated routed row normally carries the description prefix below, and that prefix is what
|
|
8
|
+
* removal, template selection, auto-review and residue detection have always recognized. A combo
|
|
9
|
+
* may replace its row description (#7229), so such a row carries a stable marker instead. Rows
|
|
10
|
+
* with the generated description stay byte-identical and keep prefix recognition; only an exact
|
|
11
|
+
* known marker value confers authorship.
|
|
12
|
+
*/
|
|
13
|
+
export const ROUTED_DESCRIPTION_PREFIX = "Routed via opencodex → ";
|
|
14
|
+
export const ROUTED_ORIGIN_FIELD = "opencodex_routed_origin";
|
|
15
|
+
export const COMBO_ROUTED_ORIGIN = "combo-v1";
|
|
16
|
+
const COMBO_DESCRIPTION_PREFIX = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (`;
|
|
17
|
+
|
|
18
|
+
function descriptionStartsWith(entry: RawEntry, prefix: string): boolean {
|
|
19
|
+
return typeof entry.description === "string" && entry.description.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True for a row whose description was overridden by a combo and stamped by OpenCodex. */
|
|
23
|
+
export function hasComboRoutedOriginMarker(entry: RawEntry): boolean {
|
|
24
|
+
return entry[ROUTED_ORIGIN_FIELD] === COMBO_ROUTED_ORIGIN;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Generated by OpenCodex routing: the stable marker, or the legacy generated description. */
|
|
28
|
+
export function hasOcxRoutedAuthorship(entry: RawEntry): boolean {
|
|
29
|
+
return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, ROUTED_DESCRIPTION_PREFIX);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generated for a combo: the stable marker, or the legacy generated combo description. */
|
|
33
|
+
export function hasOcxComboAuthorship(entry: RawEntry): boolean {
|
|
34
|
+
return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, COMBO_DESCRIPTION_PREFIX);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The operator's combo description override, if this routed model has one. */
|
|
38
|
+
export function comboDescriptionOverride(model: CatalogModel | undefined): string | undefined {
|
|
39
|
+
if (model?.provider !== COMBO_NAMESPACE) return undefined;
|
|
40
|
+
const description = typeof model.description === "string" ? model.description.trim() : "";
|
|
41
|
+
return description || undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set a routed row's description and its authorship marker together. A template clone may carry
|
|
46
|
+
* another row's marker, so the marker is always rewritten from this model's own override.
|
|
47
|
+
*/
|
|
48
|
+
export function applyRoutedDescription(entry: RawEntry, generated: string, model: CatalogModel | undefined): void {
|
|
49
|
+
const override = comboDescriptionOverride(model);
|
|
50
|
+
entry.description = override ?? generated;
|
|
51
|
+
if (override !== undefined) entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
|
|
52
|
+
else delete entry[ROUTED_ORIGIN_FIELD];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A combo row retained from the previous catalog (its fresh derivation failed, e.g. a member
|
|
57
|
+
* provider is temporarily unavailable) bypasses deriveEntry. Re-project the combo's CURRENT
|
|
58
|
+
* description onto it so clearing or changing the description in config still reaches the
|
|
59
|
+
* picker. A cleared override returns the row to the generated combo text without the marker;
|
|
60
|
+
* an unmarked row without a current override is left exactly as it was.
|
|
61
|
+
*/
|
|
62
|
+
export function reapplyRetainedComboDescription(entry: RawEntry, current: string | undefined): void {
|
|
63
|
+
const description = current?.trim();
|
|
64
|
+
if (description) {
|
|
65
|
+
entry.description = description;
|
|
66
|
+
entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
|
|
67
|
+
} else if (hasComboRoutedOriginMarker(entry)) {
|
|
68
|
+
entry.description = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (${COMBO_NAMESPACE}).`;
|
|
69
|
+
delete entry[ROUTED_ORIGIN_FIELD];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -43,6 +43,7 @@ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREA
|
|
|
43
43
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
44
44
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
45
45
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
46
|
+
import { hasComboRoutedOriginMarker, hasOcxRoutedAuthorship } from "./ownership";
|
|
46
47
|
import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
|
|
47
48
|
import { recordOwnedConfigPath } from "../../lib/config-ownership";
|
|
48
49
|
|
|
@@ -95,6 +96,12 @@ export interface CatalogModel {
|
|
|
95
96
|
* which carries no CatalogModel, so a configured displayName can never override a native name.
|
|
96
97
|
*/
|
|
97
98
|
displayName?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Display-only Codex catalog `description` override, honored for combo rows only (#7229).
|
|
101
|
+
* Unset keeps the generated "Routed via opencodex → …" text; a set value stamps the row with
|
|
102
|
+
* the ownership marker in ./ownership so removal and template selection still recognize it.
|
|
103
|
+
*/
|
|
104
|
+
description?: string;
|
|
98
105
|
owned_by?: string;
|
|
99
106
|
reasoningEfforts?: string[];
|
|
100
107
|
/** Exact CCA family evidence, retained through the in-memory discovery cache. */
|
|
@@ -297,7 +304,7 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
|
|
|
297
304
|
&& "base_instructions" in m
|
|
298
305
|
&& m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
299
306
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
300
|
-
&& !(
|
|
307
|
+
&& !hasOcxRoutedAuthorship(m),
|
|
301
308
|
) ?? null;
|
|
302
309
|
}
|
|
303
310
|
|
|
@@ -325,7 +332,7 @@ export function findSupportedNativeTemplate(catalog: RawCatalog | null): RawEntr
|
|
|
325
332
|
&& "base_instructions" in m
|
|
326
333
|
&& m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
327
334
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
328
|
-
&& !(
|
|
335
|
+
&& !hasOcxRoutedAuthorship(m),
|
|
329
336
|
) ?? null;
|
|
330
337
|
}
|
|
331
338
|
|
|
@@ -925,7 +932,9 @@ export function readCatalogBackup(catalogPath: string): RawCatalog | null {
|
|
|
925
932
|
|
|
926
933
|
export function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
|
|
927
934
|
return (catalog?.models ?? []).some(m => typeof m.slug === "string"
|
|
928
|
-
&& (m.slug.includes("/")
|
|
935
|
+
&& (m.slug.includes("/")
|
|
936
|
+
|| m.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
937
|
+
|| hasComboRoutedOriginMarker(m)));
|
|
929
938
|
}
|
|
930
939
|
|
|
931
940
|
export function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {
|
|
@@ -60,7 +60,7 @@ import { isMultiAgentV2Enabled } from "../features";
|
|
|
60
60
|
import { clampCatalogModelsToCodexSupport } from "./effort";
|
|
61
61
|
import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
|
|
62
62
|
import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
|
|
63
|
-
import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
63
|
+
import { comboCatalogDescriptions, dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
64
64
|
import {
|
|
65
65
|
withCatalogWriteSerialization,
|
|
66
66
|
type CatalogWritePermit,
|
|
@@ -530,6 +530,7 @@ function writeRetainedCatalogSync({
|
|
|
530
530
|
multiAgentV2Enabled,
|
|
531
531
|
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
532
532
|
exactComboSlugs,
|
|
533
|
+
comboDescriptions: comboCatalogDescriptions(config),
|
|
533
534
|
hasPhysicalComboProvider,
|
|
534
535
|
includeNativeOpenAi,
|
|
535
536
|
accountBoundEntries,
|
|
@@ -4,6 +4,7 @@ import type { OcxConfig } from "../../types";
|
|
|
4
4
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
5
5
|
import { isOcxAuthoredRoutedEntry } from "./build-entries";
|
|
6
6
|
import { isNativeAliasCatalogEntry } from "./metadata";
|
|
7
|
+
import { hasOcxRoutedAuthorship } from "./ownership";
|
|
7
8
|
import type { RawCatalog, RawEntry } from "./parsing";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -25,10 +26,9 @@ function routedNamespace(entry: RawEntry): string | null {
|
|
|
25
26
|
if (trustedAccountBoundNativeCatalogSlug(entry) !== undefined) return null;
|
|
26
27
|
if (isNativeAliasCatalogEntry(entry)) return COMBO_NAMESPACE;
|
|
27
28
|
// Combo aliases may be bare or use an unrelated provider namespace. Ownership selects the
|
|
28
|
-
// combo namespace only with the same
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
&& entry.description.startsWith("Routed via opencodex → ")) return COMBO_NAMESPACE;
|
|
29
|
+
// combo namespace only with the same authorship signal as routed rows (generated description or
|
|
30
|
+
// the #7229 marker a custom combo description carries).
|
|
31
|
+
if (entry.owned_by === COMBO_NAMESPACE && hasOcxRoutedAuthorship(entry)) return COMBO_NAMESPACE;
|
|
32
32
|
// Foreign rows (Cursor, user tooling) are never removed by the provider rule.
|
|
33
33
|
if (!isOcxAuthoredRoutedEntry(entry)) return null;
|
|
34
34
|
return entry.slug.slice(0, entry.slug.indexOf("/"));
|
|
@@ -213,9 +213,15 @@ function npmTarget(args: readonly string[], env?: NodeJS.ProcessEnv): NpmTarget
|
|
|
213
213
|
* reverse: they are output paths, so ambient values would write Node artifacts to
|
|
214
214
|
* arbitrary external directories on exit. ELF/Darwin loader controls (LD_* and
|
|
215
215
|
* DYLD_*) are also removed: they can load code or redirect loader output before
|
|
216
|
-
* npm runs.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
216
|
+
* npm runs. OpenSSL's startup inputs are removed with them: OPENSSL_CONF names
|
|
217
|
+
* a configuration file the crypto library reads at startup, OPENSSL_CONF_INCLUDE
|
|
218
|
+
* resolves relative includes inside that configuration, and OPENSSL_MODULES /
|
|
219
|
+
* OPENSSL_ENGINES name the directories its provider and engine search loads
|
|
220
|
+
* shared modules from. None of these is input the updater child needs, so
|
|
221
|
+
* ambient values stay out of the spawned npm. This is a denylist for
|
|
222
|
+
* process-startup/output channels, not a claim that every ambient variable is
|
|
223
|
+
* allowlisted. Ordinary OS environment, proxy settings and the supported
|
|
224
|
+
* NODE_EXTRA_CA_CERTS extension remain available.
|
|
219
225
|
*/
|
|
220
226
|
const NPM_ENV_DROP_KEYS: ReadonlySet<string> = new Set([
|
|
221
227
|
"node_options",
|
|
@@ -227,6 +233,15 @@ const NPM_ENV_DROP_KEYS: ReadonlySet<string> = new Set([
|
|
|
227
233
|
// escaping the owned temp root regardless of the isolated --cache.
|
|
228
234
|
"node_v8_coverage",
|
|
229
235
|
"node_redirect_warnings",
|
|
236
|
+
// OpenSSL startup inputs: OPENSSL_CONF points the library at an ambient
|
|
237
|
+
// startup configuration file (OPENSSL_CONF_INCLUDE resolves that config's
|
|
238
|
+
// relative includes); OPENSSL_MODULES and OPENSSL_ENGINES point its
|
|
239
|
+
// provider/engine search at ambient directories that supply loadable
|
|
240
|
+
// shared modules.
|
|
241
|
+
"openssl_conf",
|
|
242
|
+
"openssl_conf_include",
|
|
243
|
+
"openssl_modules",
|
|
244
|
+
"openssl_engines",
|
|
230
245
|
]);
|
|
231
246
|
|
|
232
247
|
function codexCliUpdateNpmEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { extractAccountIdClaims, extractEmail } from "../oauth/chatgpt";
|
|
2
|
+
import type { CodexAccount, OcxConfig } from "../types";
|
|
3
|
+
import { getCodexAccountCredential } from "./account-store";
|
|
4
|
+
import type { CodexTokens } from "./auth-collision";
|
|
5
|
+
import { captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "./main-account-cache";
|
|
6
|
+
import { codexPlanKey, extractChatgptPlanType } from "./plan";
|
|
7
|
+
|
|
8
|
+
export interface CodexCollisionIdentity {
|
|
9
|
+
accountId: string;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
plan?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizedEmail(email: string | null | undefined): string | undefined {
|
|
15
|
+
return email?.trim().toLowerCase() || undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function workspacePlan(plan: unknown): boolean {
|
|
19
|
+
return /team|business|enterprise|workspace|edu/.test(codexPlanKey(plan) ?? "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Missing member evidence cannot establish a distinct subscription within one plan bucket. */
|
|
23
|
+
export function sameCodexCollisionIdentity(a: CodexCollisionIdentity, b: CodexCollisionIdentity): boolean {
|
|
24
|
+
if (!a.accountId || a.accountId !== b.accountId || workspacePlan(a.plan) !== workspacePlan(b.plan)) return false;
|
|
25
|
+
const aEmail = normalizedEmail(a.email);
|
|
26
|
+
const bEmail = normalizedEmail(b.email);
|
|
27
|
+
return !aEmail || !bEmail || aEmail === bEmail;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Ambient scope comparison is memory-only, including when the default-home resolver would probe WSL.
|
|
31
|
+
function homeScope(): string {
|
|
32
|
+
return JSON.stringify([process.env.CODEX_HOME, process.env.HOME, process.env.USERPROFILE, process.env.OPENCODEX_HOME]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let observed: { identity: CodexCollisionIdentity; homeScope: string; generation: number } | undefined;
|
|
36
|
+
|
|
37
|
+
/** Only existing owned lifecycle observations may publish; never call with a request bearer. */
|
|
38
|
+
export function observeMainCodexCollisionIdentity(tokens?: CodexTokens): void {
|
|
39
|
+
if (!tokens) { observed = undefined; return; }
|
|
40
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.account_id !== "string"
|
|
41
|
+
|| (tokens.id_token !== undefined && typeof tokens.id_token !== "string")) return;
|
|
42
|
+
const claims = [extractAccountIdClaims(tokens.id_token), extractAccountIdClaims(tokens.access_token)];
|
|
43
|
+
if (claims.some(claim => claim.conflict)) return;
|
|
44
|
+
const accountId = tokens.account_id || claims.find(claim => claim.accountId)?.accountId;
|
|
45
|
+
if (!accountId || claims.some(claim => claim.accountId && claim.accountId !== accountId)) return;
|
|
46
|
+
const emails = new Set([extractEmail(tokens.id_token), extractEmail(undefined, tokens.access_token)]
|
|
47
|
+
.flatMap(email => normalizedEmail(email) ? [normalizedEmail(email)!] : []));
|
|
48
|
+
if (emails.size > 1) return;
|
|
49
|
+
observed = { identity: { accountId, email: [...emails][0],
|
|
50
|
+
plan: extractChatgptPlanType(tokens.id_token, tokens.access_token) },
|
|
51
|
+
homeScope: homeScope(), generation: captureMainAccountIdentityGeneration() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compare known identity only. External logins become visible at the next owned observation. */
|
|
55
|
+
export function getObservedMainCodexCollisionIdentity(): Readonly<CodexCollisionIdentity> | undefined {
|
|
56
|
+
return observed?.homeScope === homeScope() && isMainAccountIdentityGenerationLive(observed.generation)
|
|
57
|
+
? observed.identity : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isStoredCodexMainDuplicate(account: CodexAccount): boolean {
|
|
61
|
+
const main = getObservedMainCodexCollisionIdentity();
|
|
62
|
+
if (account.isMain || !main) return false;
|
|
63
|
+
const credential = getCodexAccountCredential(account.id);
|
|
64
|
+
if (!credential?.chatgptAccountId) return false;
|
|
65
|
+
const storedEmail = normalizedEmail(account.email);
|
|
66
|
+
const email = storedEmail && storedEmail !== normalizedEmail(account.id)
|
|
67
|
+
? storedEmail : extractEmail(undefined, credential.accessToken);
|
|
68
|
+
return sameCodexCollisionIdentity({ accountId: credential.chatgptAccountId, email,
|
|
69
|
+
plan: codexPlanKey(account.plan) ? account.plan : extractChatgptPlanType(undefined, credential.accessToken) }, main);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const CODEX_MAIN_DUPLICATE_ACTION =
|
|
73
|
+
"Use the native Codex main account, or sign native Codex into a different account to use this stored Pool entry.";
|
|
74
|
+
|
|
75
|
+
export function configuredCodexMainDuplicate(config: Pick<OcxConfig, "codexAccounts">, accountId: string): boolean {
|
|
76
|
+
const account = config.codexAccounts?.find(row => row.id === accountId);
|
|
77
|
+
return !!account && isStoredCodexMainDuplicate(account);
|
|
78
|
+
}
|