@bitkyc08/opencodex 2.57.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -97,20 +97,60 @@ function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
100
|
+
* Whether a request carrying its OWN main credential still serves on main because the operator
|
|
101
|
+
* manually pinned it (#3166), split from the surrounding resolution so request preview can ask
|
|
102
|
+
* the identical question (#4850).
|
|
101
103
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* Exported for exactly one reason: two copies of this fence is how #4850 happened. Final
|
|
105
|
+
* authentication honoured the ownership boundary while request preview, computing its fence from
|
|
106
|
+
* recovery and drain state alone, still handed pool eligibility the default liveness probe and
|
|
107
|
+
* opened the physical `auth.json` twice per spawn. A predicate one caller can forget is a
|
|
108
|
+
* predicate the other caller will eventually disagree with.
|
|
107
109
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `
|
|
110
|
+
* Read-free by construction, which is what makes it usable on the fenced side. Every input is
|
|
111
|
+
* config, policy, or in-memory runtime state: the pin fields, the paused list, the cached quota
|
|
112
|
+
* score, and `callerMatchesObservedMain`, which compares HMAC digests against the observed
|
|
113
|
+
* credential record in `main-account-cache.ts`. Nothing here opens a file.
|
|
114
|
+
*
|
|
115
|
+
* `candidate` is the pin before the hard-lock question, because the caller still owes the
|
|
116
|
+
* pending-binding check that only final authentication can fail closed on.
|
|
117
|
+
*/
|
|
118
|
+
export function requestOwnedMainPinState(
|
|
119
|
+
headers: Headers,
|
|
120
|
+
config: OcxConfig,
|
|
121
|
+
policy: CodexAuthPolicyConfig,
|
|
122
|
+
requestScopedMainCredential: boolean,
|
|
123
|
+
fixedAccountId: string | undefined,
|
|
124
|
+
): { candidate: boolean; preserve: boolean } {
|
|
125
|
+
const candidate = requestScopedMainCredential
|
|
126
|
+
&& fixedAccountId === undefined
|
|
127
|
+
&& config.activeCodexAccountPinned === MAIN_CODEX_ACCOUNT_ID
|
|
128
|
+
&& isEffectiveCodexAccountPinned(config)
|
|
129
|
+
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
130
|
+
&& requestOwnedMainPinHasQuotaHeadroom(config);
|
|
131
|
+
return {
|
|
132
|
+
candidate,
|
|
133
|
+
preserve: candidate && !(callerMatchesObservedMain(headers) && isMainAccountHardLocked(policy)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One conversation tree keys as ONE cohort (#4780).
|
|
139
|
+
*
|
|
140
|
+
* Upstream keys its prompt cache on something the whole tree shares -- `prompt_cache_key` is the
|
|
141
|
+
* session id, or `{source}:{parent_thread_id}` for an internal session -- and one `AgentControl`
|
|
142
|
+
* whose `session_id` is the root thread's id is shared with every sub-agent. While the proxy
|
|
143
|
+
* keyed per thread, two requests could carry an identical `prompt_cache_key` and be served by
|
|
144
|
+
* different accounts, so the split member asserted a warm prefix that was deterministically cold
|
|
145
|
+
* on its account. Nothing failed; the prompt was replayed in full and the tokens burned.
|
|
146
|
+
*
|
|
147
|
+
* THIS IS NOT A REVERT OF #4546 wp8. wp8 fixed a different defect -- a child bound under the RAW
|
|
148
|
+
* parent id, an identity unrelated to the root's own binding, so siblings shared an entry the
|
|
149
|
+
* root was not on and a grandchild landed on a key nobody had bound. A cohort key has no such
|
|
150
|
+
* incoherence, because the root's own binding IS the cohort key. What wp8 additionally gave each
|
|
151
|
+
* thread, a binding of its own, is what this deliberately gives up.
|
|
152
|
+
*
|
|
153
|
+
* Which requests produce no key at all is unchanged, through both units.
|
|
114
154
|
*
|
|
115
155
|
* The derivation itself lives in ./lineage so a lineage record's conversation key and the key
|
|
116
156
|
* the thread actually binds under can never drift apart.
|
|
@@ -816,19 +856,15 @@ export async function resolveCodexAuthContext(
|
|
|
816
856
|
throw new CodexReserveUnavailableError();
|
|
817
857
|
}
|
|
818
858
|
const fixedAccountId = reserve ? MAIN_CODEX_ACCOUNT_ID : options.accountId;
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
824
|
-
&& requestOwnedMainPinHasQuotaHeadroom(config);
|
|
859
|
+
const {
|
|
860
|
+
candidate: requestOwnedMainPinCandidate,
|
|
861
|
+
preserve: preserveRequestOwnedMainPin,
|
|
862
|
+
} = requestOwnedMainPinState(headers, config, policy, requestScopedMainCredential, fixedAccountId);
|
|
825
863
|
// During an owned startup, equality cannot be established until recovery and the
|
|
826
864
|
// memory-only policy binding finish. This read-only fence never probes a foreign home.
|
|
827
865
|
if (policy.codexMainAccountHardLock === true && requestOwnedMainPinCandidate && isMainAccountPolicyBindingPending()) {
|
|
828
866
|
throw new CodexMainProfileDrainingError();
|
|
829
867
|
}
|
|
830
|
-
const preserveRequestOwnedMainPin = requestOwnedMainPinCandidate
|
|
831
|
-
&& !(callerMatchesObservedMain(headers) && isMainAccountHardLocked(policy));
|
|
832
868
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
833
869
|
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
834
870
|
}
|
|
@@ -1385,6 +1421,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1385
1421
|
if (!stored?.accessToken || !isMainAccountTokenLive()) {
|
|
1386
1422
|
throw new CodexMainSubstitutionUnavailableError();
|
|
1387
1423
|
}
|
|
1424
|
+
selected.delete("chatgpt-account-id");
|
|
1388
1425
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
1389
1426
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1390
1427
|
observeSelectedMainCredential(stored, writer);
|
|
@@ -1462,6 +1499,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1462
1499
|
...(options.nativeMainRefreshDependencies ?? {}),
|
|
1463
1500
|
});
|
|
1464
1501
|
if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
|
|
1502
|
+
selected.delete("chatgpt-account-id");
|
|
1465
1503
|
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
1466
1504
|
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
1467
1505
|
observeSelectedMainCredential(stored, writer);
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "./metadata";
|
|
27
27
|
import { resetBundledCatalogCacheForTests } from "./bundled";
|
|
28
28
|
import { isMultiAgentV2Enabled } from "../features";
|
|
29
|
-
import { ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
29
|
+
import { clampedDefaultEffort, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
30
30
|
import { clearGatherRoutedModelsInflight, lastDropWarnSignature } from "./provider-fetch";
|
|
31
31
|
import {
|
|
32
32
|
accountSelectorShadowCollisionWarnings,
|
|
@@ -490,10 +490,14 @@ export interface ObservedCatalogMergeInput {
|
|
|
490
490
|
readonly includeNativeOpenAi: boolean;
|
|
491
491
|
readonly accountBoundEntries: readonly RawEntry[];
|
|
492
492
|
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
493
|
+
/** Routed slugs that must not gain a missing synthetic max rung during retained-row repair. */
|
|
494
|
+
readonly suppressedSyntheticMaxSlugs?: ReadonlySet<string>;
|
|
493
495
|
readonly policy: ObservedCatalogMergePolicy;
|
|
494
496
|
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
495
497
|
/** Exact display-only labels for bare native OpenAI models. */
|
|
496
498
|
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
|
|
499
|
+
/** Pristine installed-catalog multi-agent pins; see MultiAgentModeOptions.nativeDefaults. */
|
|
500
|
+
readonly nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>;
|
|
497
501
|
}
|
|
498
502
|
|
|
499
503
|
/**
|
|
@@ -526,9 +530,11 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
526
530
|
includeNativeOpenAi,
|
|
527
531
|
accountBoundEntries,
|
|
528
532
|
suppressedBareNativeSlugs = new Set(),
|
|
533
|
+
suppressedSyntheticMaxSlugs = new Set(),
|
|
529
534
|
policy,
|
|
530
535
|
openaiContextCap,
|
|
531
536
|
nativeDisplayNames,
|
|
537
|
+
nativeMultiAgentDefaults,
|
|
532
538
|
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
533
539
|
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
534
540
|
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
@@ -860,16 +866,29 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
860
866
|
});
|
|
861
867
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
862
868
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
863
|
-
// reasoning-capable entry.
|
|
869
|
+
// reasoning-capable entry. A suppressed preserved row that already contains max keeps it;
|
|
870
|
+
// without persisted provenance, only a healthy provider rebuild can distinguish and remove
|
|
871
|
+
// an older synthetic rung from a real provider-declared rung. max only: 5.6 exact ladders
|
|
872
|
+
// (luna: no ultra) stay intact.
|
|
864
873
|
if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
865
874
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
866
875
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
867
876
|
: [];
|
|
868
|
-
if (levels.length > 0
|
|
877
|
+
if (levels.length > 0
|
|
878
|
+
&& !suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
|
|
879
|
+
&& !levels.some(level => level.effort === "max")) {
|
|
869
880
|
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
|
|
870
881
|
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
|
|
871
882
|
e.supported_reasoning_levels = levels;
|
|
872
883
|
}
|
|
884
|
+
if (suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
|
|
885
|
+
&& typeof e.default_reasoning_level === "string"
|
|
886
|
+
&& !levels.some(level => level.effort === e.default_reasoning_level)) {
|
|
887
|
+
e.default_reasoning_level = clampedDefaultEffort(
|
|
888
|
+
e.default_reasoning_level,
|
|
889
|
+
levels.flatMap(level => typeof level.effort === "string" ? [level.effort] : []),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
873
892
|
}
|
|
874
893
|
if (wsEnabled) e.supports_websockets = true;
|
|
875
894
|
else {
|
|
@@ -886,7 +905,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
886
905
|
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
887
906
|
multiAgentMode,
|
|
888
907
|
multiAgentV2Enabled,
|
|
889
|
-
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
|
|
908
|
+
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
|
|
890
909
|
);
|
|
891
910
|
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
|
|
892
911
|
for (const entry of versionedEntries) {
|
|
@@ -937,6 +956,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
937
956
|
),
|
|
938
957
|
openaiContextCap?: NativeContextLimitsInput,
|
|
939
958
|
keepNativeChatGptOnV1 = false,
|
|
959
|
+
nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>,
|
|
940
960
|
): RawEntry[] {
|
|
941
961
|
// Retained for source compatibility with the original helper contract. Raw provider ids must
|
|
942
962
|
// not suppress same-named native rows; actual admitted combo entries own that decision now.
|
|
@@ -973,6 +993,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
973
993
|
accountBoundEntries,
|
|
974
994
|
suppressedBareNativeSlugs,
|
|
975
995
|
openaiContextCap,
|
|
996
|
+
nativeMultiAgentDefaults,
|
|
976
997
|
policy: {
|
|
977
998
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
978
999
|
warningPolicy: "emit",
|
|
@@ -160,6 +160,7 @@ export function deriveEntry(
|
|
|
160
160
|
preserveExactReasoning
|
|
161
161
|
|| codexForwardNativeCapabilityAlias !== null
|
|
162
162
|
|| preservePinnedNativeCustomReasoning(model),
|
|
163
|
+
model?.suppressSyntheticMax === true,
|
|
163
164
|
);
|
|
164
165
|
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
165
166
|
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
@@ -208,7 +209,13 @@ export function deriveEntry(
|
|
|
208
209
|
};
|
|
209
210
|
if (isRouted) {
|
|
210
211
|
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
211
|
-
applyReasoningLevels(
|
|
212
|
+
applyReasoningLevels(
|
|
213
|
+
entry,
|
|
214
|
+
model?.reasoningEfforts,
|
|
215
|
+
model?.defaultReasoningEffort,
|
|
216
|
+
preserveExactReasoning || preservePinnedNativeCustomReasoning(model),
|
|
217
|
+
model?.suppressSyntheticMax === true,
|
|
218
|
+
);
|
|
212
219
|
}
|
|
213
220
|
else {
|
|
214
221
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
@@ -226,10 +226,12 @@ export function applyReasoningLevels(
|
|
|
226
226
|
effortsOverride?: string[],
|
|
227
227
|
defaultOverride?: string,
|
|
228
228
|
preserveExact = false,
|
|
229
|
+
suppressSyntheticMax = false,
|
|
229
230
|
): void {
|
|
230
231
|
let efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
|
|
231
|
-
// Mock top tiers (user decision 260709):
|
|
232
|
-
// even when the provider ladder stops lower
|
|
232
|
+
// Mock top tiers (user decision 260709): reasoning-capable routed models advertise `max`
|
|
233
|
+
// even when the provider ladder stops lower, unless the model opts out of that synthesis.
|
|
234
|
+
// Subagent spawns pass `max` DIRECTLY
|
|
233
235
|
// (no ultra->max client conversion) and codex-rs validates it by catalog membership,
|
|
234
236
|
// so a missing max rung hard-fails spawn_agent effort overrides. The wire stays honest:
|
|
235
237
|
// routed adapters clamp via clampToSupportedCodexEffort and natives via
|
|
@@ -237,7 +239,7 @@ export function applyReasoningLevels(
|
|
|
237
239
|
// reasoning-capable, so it must not grow synthetic top rungs.
|
|
238
240
|
if (!preserveExact && efforts.length > 0 && efforts.some(effort => effort !== "none" && effort !== "minimal")) {
|
|
239
241
|
const additions: string[] = [];
|
|
240
|
-
if (!efforts.includes("max")) additions.push("max");
|
|
242
|
+
if (!suppressSyntheticMax && !efforts.includes("max")) additions.push("max");
|
|
241
243
|
if (!efforts.includes("ultra")) additions.push("ultra");
|
|
242
244
|
if (additions.length > 0) efforts = sanitizeCodexReasoningEfforts([...efforts, ...additions]) ?? efforts;
|
|
243
245
|
}
|
|
@@ -258,9 +260,11 @@ export function applyReasoningLevels(
|
|
|
258
260
|
}
|
|
259
261
|
entry.default_reasoning_level = defaultOverride && efforts.includes(defaultOverride)
|
|
260
262
|
? defaultOverride
|
|
261
|
-
:
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
: suppressSyntheticMax && defaultOverride === "max"
|
|
264
|
+
? clampedDefaultEffort(defaultOverride, efforts)
|
|
265
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high"
|
|
266
|
+
// Sentinels never become the implicit default when real rungs are declared.
|
|
267
|
+
: efforts.find(effort => effort !== "none" && effort !== "minimal") ?? efforts[0];
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
/**
|
|
@@ -499,6 +499,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
499
499
|
inMod: prov.modelInputModalities ?? null,
|
|
500
500
|
capabilities: prov.modelCapabilities ?? null,
|
|
501
501
|
re: prov.modelReasoningEfforts ?? null,
|
|
502
|
+
suppressMax: prov.modelSuppressSyntheticMax ?? null,
|
|
502
503
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
503
504
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
504
505
|
verbosity: prov.modelSupportsVerbosity ?? null,
|
|
@@ -296,6 +296,7 @@ export function applyProviderConfigHints(
|
|
|
296
296
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
297
297
|
}
|
|
298
298
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
299
|
+
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
|
|
299
300
|
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
300
301
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
301
302
|
const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
|
|
@@ -305,6 +306,7 @@ export function applyProviderConfigHints(
|
|
|
305
306
|
supportsServiceTier: _staleServiceTier,
|
|
306
307
|
fastTierDescription: _staleFastTierDescription,
|
|
307
308
|
providerAlias: _staleProviderAlias,
|
|
309
|
+
suppressSyntheticMax: _staleSuppressSyntheticMax,
|
|
308
310
|
...modelWithoutServiceTier
|
|
309
311
|
} = model;
|
|
310
312
|
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
|
|
@@ -321,6 +323,7 @@ export function applyProviderConfigHints(
|
|
|
321
323
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
322
324
|
...(inputModalities ? { inputModalities } : {}),
|
|
323
325
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
326
|
+
...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
|
|
324
327
|
...(configuredMaxInput !== undefined
|
|
325
328
|
? {
|
|
326
329
|
maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
@@ -394,6 +397,31 @@ export function applyConfigHintsToCachedModels(
|
|
|
394
397
|
): CatalogModel[] {
|
|
395
398
|
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
|
|
396
399
|
}
|
|
400
|
+
|
|
401
|
+
/** Catalog slugs whose configured model must not gain a missing synthetic max rung. */
|
|
402
|
+
export function suppressedSyntheticMaxCatalogSlugs(
|
|
403
|
+
config: Pick<OcxConfig, "providers">,
|
|
404
|
+
models: readonly CatalogModel[],
|
|
405
|
+
observedEntries: readonly { slug?: unknown }[] = [],
|
|
406
|
+
): ReadonlySet<string> {
|
|
407
|
+
const slugs = new Set(models
|
|
408
|
+
.filter(model => model.suppressSyntheticMax === true)
|
|
409
|
+
.map(catalogModelSlug));
|
|
410
|
+
for (const [provider, providerConfig] of Object.entries(config.providers)) {
|
|
411
|
+
const configured = providerConfig.modelSuppressSyntheticMax ?? {};
|
|
412
|
+
const encoded = Object.fromEntries(Object.entries(configured)
|
|
413
|
+
.map(([modelId, suppress]) => [routedSlug(provider, modelId).slice(provider.length + 1), suppress]));
|
|
414
|
+
for (const [modelId, suppress] of Object.entries(configured)) {
|
|
415
|
+
if (suppress === true) slugs.add(routedSlug(provider, modelId));
|
|
416
|
+
}
|
|
417
|
+
for (const entry of observedEntries) {
|
|
418
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
419
|
+
if (!slug.startsWith(`${provider}/`)) continue;
|
|
420
|
+
if (modelRecordValue(encoded, slug.slice(provider.length + 1)) === true) slugs.add(slug);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return slugs;
|
|
424
|
+
}
|
|
397
425
|
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
398
426
|
|
|
399
427
|
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
@@ -582,10 +610,11 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
582
610
|
/**
|
|
583
611
|
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
584
612
|
*
|
|
585
|
-
* Fail closed.
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
613
|
+
* Fail closed. Any positive numeric component proves the model is paid. Calling it free requires
|
|
614
|
+
* a complete prompt/completion pair and every published pricing component to be a non-negative
|
|
615
|
+
* numeric zero; an unsupported component is "unknown" because it may describe another charge.
|
|
616
|
+
* Showing a paid model under a Free filter spends the user's money, while hiding a free one costs
|
|
617
|
+
* a click.
|
|
589
618
|
*
|
|
590
619
|
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
591
620
|
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
@@ -598,10 +627,13 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
598
627
|
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
599
628
|
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
600
629
|
if (!pricing) return "unknown";
|
|
630
|
+
const rates = Object.values(pricing).map(discoveredPricingRate);
|
|
631
|
+
if (rates.some(rate => rate !== undefined && rate > 0)) return "paid";
|
|
632
|
+
if (rates.some(rate => rate === undefined)) return "unknown";
|
|
601
633
|
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
602
634
|
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
603
635
|
if (prompt === undefined || completion === undefined) return "unknown";
|
|
604
|
-
return
|
|
636
|
+
return "free";
|
|
605
637
|
}
|
|
606
638
|
|
|
607
639
|
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
@@ -112,6 +112,8 @@ export interface CatalogModel {
|
|
|
112
112
|
displayName?: string;
|
|
113
113
|
owned_by?: string;
|
|
114
114
|
reasoningEfforts?: string[];
|
|
115
|
+
/** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
|
|
116
|
+
suppressSyntheticMax?: boolean;
|
|
115
117
|
defaultReasoningEffort?: string;
|
|
116
118
|
contextWindow?: number;
|
|
117
119
|
maxInputTokens?: number;
|
|
@@ -255,12 +257,19 @@ export function readCodexCatalogPath(): string {
|
|
|
255
257
|
return activeDefaultCatalogPath();
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Resolve the configured catalog without consulting ambient CODEX_HOME again.
|
|
262
|
+
*
|
|
263
|
+
* `configText` is for a caller that has already read that same `config.toml` under
|
|
264
|
+
* its own constraints - the prompt-text probe reads it bounded, on the request
|
|
265
|
+
* thread - so resolving the catalog does not cost a second, unbounded read of the
|
|
266
|
+
* file the caller is holding. Omitting it keeps the original behaviour.
|
|
267
|
+
*/
|
|
268
|
+
export function readCodexCatalogPathForHome(codexHome: string, configText?: string): string {
|
|
260
269
|
try {
|
|
261
270
|
const configPath = join(codexHome, "config.toml");
|
|
262
|
-
if (existsSync(configPath)) {
|
|
263
|
-
const toml = readFileSync(configPath, "utf-8");
|
|
271
|
+
if (configText !== undefined || existsSync(configPath)) {
|
|
272
|
+
const toml = configText ?? readFileSync(configPath, "utf-8");
|
|
264
273
|
const path = readRootTomlString(toml, "model_catalog_json");
|
|
265
274
|
if (path) return resolve(codexHome, path);
|
|
266
275
|
}
|
|
@@ -284,6 +293,30 @@ export function readConfiguredAutoReviewModel(): string | null {
|
|
|
284
293
|
return null;
|
|
285
294
|
}
|
|
286
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Read the root `model` pin from Codex's config.toml (issue #4646).
|
|
298
|
+
*
|
|
299
|
+
* Codex starts every new session on this id, and nothing in opencodex checks that the id is one
|
|
300
|
+
* the proxy actually exposes: the pin lives in Codex's config, while exposure is decided here by
|
|
301
|
+
* `disabledModels`, provider `selectedModels`, and account entitlements. When the two disagree
|
|
302
|
+
* every turn fails and no surface says why, which is what the `ocx doctor` section added for
|
|
303
|
+
* #4646 reports.
|
|
304
|
+
*
|
|
305
|
+
* Read-only, and deliberately the same shape and the same swallow-and-return-null error policy as
|
|
306
|
+
* `readConfiguredAutoReviewModel` above: a diagnostic must degrade to "unknown" on an unreadable
|
|
307
|
+
* or absent config rather than throw out of the surface that called it.
|
|
308
|
+
*/
|
|
309
|
+
export function readConfiguredDefaultModel(): string | null {
|
|
310
|
+
try {
|
|
311
|
+
const configPath = activeCodexConfigPath();
|
|
312
|
+
if (existsSync(configPath)) {
|
|
313
|
+
const toml = readFileSync(configPath, "utf-8");
|
|
314
|
+
return readRootTomlString(toml, "model");
|
|
315
|
+
}
|
|
316
|
+
} catch { /* ignore */ }
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
287
320
|
export function parseCatalogJson(raw: string): RawCatalog | null {
|
|
288
321
|
try {
|
|
289
322
|
const cat = JSON.parse(raw);
|
|
@@ -627,6 +660,14 @@ export interface MultiAgentModeOptions {
|
|
|
627
660
|
* so they can still spawn Grok/Claude — ChatGPT encrypts v2 NEW_TASK bodies.
|
|
628
661
|
*/
|
|
629
662
|
keepNativeChatGptOnV1?: boolean;
|
|
663
|
+
/**
|
|
664
|
+
* Pristine installed-catalog pins keyed by bare native slug. When provided, the
|
|
665
|
+
* backup — not the bundled snapshot — is authoritative for the rows it contains,
|
|
666
|
+
* and a preserved live/native row outside it keeps the pin it already carries:
|
|
667
|
+
* an absent baseline entry cannot distinguish a stale forced stamp from a
|
|
668
|
+
* legitimate user- or provider-preserved pin, so the non-destructive read wins.
|
|
669
|
+
*/
|
|
670
|
+
nativeDefaults?: ReadonlyMap<string, string | null>;
|
|
630
671
|
}
|
|
631
672
|
|
|
632
673
|
/** Catalog rows that run on the ChatGPT backend (encrypt v2 child tasks). */
|
|
@@ -707,13 +748,34 @@ export function applyMultiAgentMode(
|
|
|
707
748
|
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
|
|
708
749
|
? routedNativeSlug
|
|
709
750
|
: undefined;
|
|
751
|
+
const accountBoundNativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
752
|
+
const nativeLookupSlug = accountBoundNativeSlug ?? slug;
|
|
753
|
+
// The baseline is built from bare native slugs only, so "absent from the
|
|
754
|
+
// baseline" is not evidence about a routed row — it is guaranteed. Only a
|
|
755
|
+
// native row can carry a pin the baseline legitimately failed to mention;
|
|
756
|
+
// a routed row keeps the documented default-mode normalization.
|
|
757
|
+
const isNativeCatalogEntry = accountBoundNativeSlug !== undefined || !slug.includes("/");
|
|
758
|
+
const hasNativeDefault = !nativeAlias
|
|
759
|
+
&& codexForwardCapabilityAlias === undefined
|
|
760
|
+
&& options.nativeDefaults?.has(nativeLookupSlug) === true;
|
|
710
761
|
const upstreamPin = nativeAlias
|
|
711
762
|
? nativeMultiAgentVersion(slug)
|
|
712
763
|
: codexForwardCapabilityAlias
|
|
713
764
|
? nativeMultiAgentVersion(codexForwardCapabilityAlias)
|
|
714
|
-
:
|
|
765
|
+
: hasNativeDefault
|
|
766
|
+
? options.nativeDefaults?.get(nativeLookupSlug)
|
|
767
|
+
: options.nativeDefaults === undefined
|
|
768
|
+
? UPSTREAM_NATIVE_ENTRIES.get(nativeLookupSlug)?.multi_agent_version
|
|
769
|
+
: undefined;
|
|
715
770
|
if (typeof upstreamPin === "string") {
|
|
716
771
|
entry.multi_agent_version = upstreamPin;
|
|
772
|
+
} else if (options.nativeDefaults !== undefined
|
|
773
|
+
&& !nativeAlias
|
|
774
|
+
&& codexForwardCapabilityAlias === undefined
|
|
775
|
+
&& isNativeCatalogEntry
|
|
776
|
+
&& !hasNativeDefault
|
|
777
|
+
&& typeof entry.multi_agent_version === "string") {
|
|
778
|
+
continue;
|
|
717
779
|
} else if (v2FeatureEnabled) {
|
|
718
780
|
entry.multi_agent_version = "v2";
|
|
719
781
|
} else {
|
|
@@ -888,3 +950,19 @@ export function readNativeBaseline(catalogPath: string): Map<string, number> {
|
|
|
888
950
|
}
|
|
889
951
|
return out;
|
|
890
952
|
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Extract the pristine baseline's per-slug multi-agent pins. A bare native row that
|
|
956
|
+
* carried no pin maps to null so "baseline says unpinned" stays distinguishable
|
|
957
|
+
* from "baseline never contained this row".
|
|
958
|
+
*/
|
|
959
|
+
export function nativeMultiAgentDefaults(
|
|
960
|
+
models: readonly Readonly<Record<string, unknown>>[] | null | undefined,
|
|
961
|
+
): Map<string, string | null> {
|
|
962
|
+
const out = new Map<string, string | null>();
|
|
963
|
+
for (const entry of models ?? []) {
|
|
964
|
+
if (typeof entry.slug !== "string" || entry.slug.includes("/")) continue;
|
|
965
|
+
out.set(entry.slug, typeof entry.multi_agent_version === "string" ? entry.multi_agent_version : null);
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isEffectiveCodexDesktopAuthless } from "../loopback-target";
|
|
3
|
+
import { NATIVE_RESERVE_MODEL } from "./native-models";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #4811: why an opted-in install still gets no Luna Reserve row.
|
|
7
|
+
*
|
|
8
|
+
* The Reserve projection is account-qualified — its slug is `<selector>/gpt-reserve` — so it
|
|
9
|
+
* cannot be written without a selector that targets the main Codex account. On a fresh authless
|
|
10
|
+
* install `codexAccountNamespaces` is empty, the account picker is therefore off, no selectors
|
|
11
|
+
* exist, and `createReserveCatalogProjection` returns `undefined`. Nothing downstream can
|
|
12
|
+
* explain that: an omitted row has no place to carry a reason, which is exactly the shape of
|
|
13
|
+
* #4212 for account-gated natives, and the same answer applies — say it once, here, while the
|
|
14
|
+
* inputs that produced the omission are still in scope.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately scoped to an operator who asked for the feature. A default install has
|
|
17
|
+
* `codexDesktopAuthless` unset, wants no Reserve row, and must not be told about one. An install
|
|
18
|
+
* that stores the flag on a bind where it cannot take effect is a different silence, already
|
|
19
|
+
* reported as `inertReason` by `describeCodexDesktopSwitches`, and is not repeated here.
|
|
20
|
+
*/
|
|
21
|
+
export type ReserveCatalogSuppressionReason =
|
|
22
|
+
| "no-canonical-openai-provider"
|
|
23
|
+
| "account-picker-disabled"
|
|
24
|
+
| "no-account-namespaces"
|
|
25
|
+
| "no-main-account-selector";
|
|
26
|
+
|
|
27
|
+
type ReserveSuppressionConfig = Pick<
|
|
28
|
+
OcxConfig,
|
|
29
|
+
| "runtimeRole"
|
|
30
|
+
| "hostname"
|
|
31
|
+
| "unauthenticatedLoopbackListener"
|
|
32
|
+
| "codexDesktopAuthless"
|
|
33
|
+
| "codexAccountNamespaces"
|
|
34
|
+
| "codexAccountPickerEnabled"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export interface ReserveSuppressionInput {
|
|
38
|
+
/** Already resolved by the caller; passed in so this module stays off the metadata graph. */
|
|
39
|
+
includeAccountBoundNativeOpenAi: boolean;
|
|
40
|
+
/** Selectors that resolved to the main Codex account. A non-empty list means no suppression. */
|
|
41
|
+
mainSelectors: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reserveCatalogSuppressionReason(
|
|
45
|
+
config: ReserveSuppressionConfig,
|
|
46
|
+
input: ReserveSuppressionInput,
|
|
47
|
+
): ReserveCatalogSuppressionReason | undefined {
|
|
48
|
+
if (!isEffectiveCodexDesktopAuthless(config) || input.mainSelectors.length > 0) return undefined;
|
|
49
|
+
if (!input.includeAccountBoundNativeOpenAi) return "no-canonical-openai-provider";
|
|
50
|
+
// Checked before the empty-map case: when an operator has explicitly turned the picker off,
|
|
51
|
+
// naming the empty map would send them to hand-author selectors that the opt-in generates.
|
|
52
|
+
if (config.codexAccountPickerEnabled === false) return "account-picker-disabled";
|
|
53
|
+
if (Object.keys(config.codexAccountNamespaces ?? {}).length === 0) return "no-account-namespaces";
|
|
54
|
+
return "no-main-account-selector";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SUPPRESSION_TEXT: Readonly<Record<ReserveCatalogSuppressionReason, {
|
|
58
|
+
cause: string;
|
|
59
|
+
action: string;
|
|
60
|
+
}>> = {
|
|
61
|
+
"no-canonical-openai-provider": {
|
|
62
|
+
cause: "no enabled canonical OpenAI provider can serve account-qualified native routes",
|
|
63
|
+
action: "Enable the built-in \"openai\" provider with forwarded ChatGPT auth.",
|
|
64
|
+
},
|
|
65
|
+
"account-picker-disabled": {
|
|
66
|
+
cause: "the Codex account picker is turned off (codexAccountPickerEnabled is false)",
|
|
67
|
+
action: "Set codexAccountPickerEnabled to true to generate the default account selectors.",
|
|
68
|
+
},
|
|
69
|
+
"no-account-namespaces": {
|
|
70
|
+
cause: "no Codex account selectors are configured (codexAccountNamespaces is empty)",
|
|
71
|
+
action: "Enable the Codex account picker once to generate the default account selectors.",
|
|
72
|
+
},
|
|
73
|
+
"no-main-account-selector": {
|
|
74
|
+
cause: "no configured selector in codexAccountNamespaces targets the main Codex account",
|
|
75
|
+
action: "Map one selector to the main Codex account.",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const warnedReserveSuppression = new Set<string>();
|
|
80
|
+
|
|
81
|
+
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
82
|
+
export function resetReserveSuppressionWarningsForTests(): void {
|
|
83
|
+
warnedReserveSuppression.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function warnReserveSuppressedOnce(reason: ReserveCatalogSuppressionReason): void {
|
|
87
|
+
if (warnedReserveSuppression.has(reason)) return;
|
|
88
|
+
warnedReserveSuppression.add(reason);
|
|
89
|
+
const text = SUPPRESSION_TEXT[reason];
|
|
90
|
+
console.warn(
|
|
91
|
+
`[opencodex] catalog sync: authless Codex Desktop routing is on, but the Luna Reserve row `
|
|
92
|
+
+ `(<selector>/${NATIVE_RESERVE_MODEL}) is not being written because ${text.cause}. `
|
|
93
|
+
+ `The Reserve projection is account-qualified and needs a selector for the main Codex `
|
|
94
|
+
+ `account. ${text.action}`,
|
|
95
|
+
);
|
|
96
|
+
}
|