@bitkyc08/opencodex 2.58.0 → 2.60.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-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- 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 +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- 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 +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -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/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- 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/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- 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 +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- 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 +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -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-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -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 +289 -16
- 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 +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -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 +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- 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 +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- 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 +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -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/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- 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/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -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
|
/**
|
|
@@ -100,8 +100,9 @@ import type {
|
|
|
100
100
|
CatalogSourceEvidence,
|
|
101
101
|
CatalogTrustedOpenAiApiPolicySnapshot,
|
|
102
102
|
} from "../convergence-types";
|
|
103
|
-
import {
|
|
103
|
+
import { modelCapabilities, modelInputModalities } from "./model-hints";
|
|
104
104
|
import { configuredComboTargetModelsByProvider } from "./combo-member";
|
|
105
|
+
import { resolveModelPolicy } from "../../providers/resolved-model-policy";
|
|
105
106
|
|
|
106
107
|
/** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
|
|
107
108
|
* Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
|
|
@@ -344,8 +345,26 @@ export function captureProviderGather(
|
|
|
344
345
|
): CapturedProviderGather {
|
|
345
346
|
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
|
|
346
347
|
enrichProviderFromRegistry(name, enriched);
|
|
347
|
-
applyRegistryCapabilitySeedFill(name, enriched);
|
|
348
348
|
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
|
|
349
|
+
const registryEntry = registryTransportMatch ? getProviderRegistryEntry(name) : undefined;
|
|
350
|
+
const staticProvider = resolveModelPolicy({
|
|
351
|
+
providerName: name,
|
|
352
|
+
modelId: enriched.defaultModel ?? "__catalog_capture__",
|
|
353
|
+
provider: enriched,
|
|
354
|
+
registryEntry,
|
|
355
|
+
transportMatchedRegistry: registryTransportMatch,
|
|
356
|
+
...(enriched.authMode ? { effectiveAuth: { authMode: enriched.authMode } } : {}),
|
|
357
|
+
}).provider;
|
|
358
|
+
for (const key of [
|
|
359
|
+
"modelContextWindows", "modelInputModalities", "modelMaxInputTokens", "modelMaxOutputTokens",
|
|
360
|
+
"modelReasoningEfforts", "modelSupportsReasoningSummaries",
|
|
361
|
+
"modelSupportsVerbosity", "modelSupportsServiceTier",
|
|
362
|
+
] as const) {
|
|
363
|
+
if (staticProvider[key] !== undefined) enriched[key] = detachedClone(staticProvider[key]) as never;
|
|
364
|
+
}
|
|
365
|
+
// A present modelDefaultReasoningEfforts object is a whole-map catalog authority, including
|
|
366
|
+
// explicit {}. enrichProviderFromRegistry already preserves/fills that contract; replacing it
|
|
367
|
+
// with the resolver's per-key registry fill would turn "no configured default" into a seed default.
|
|
349
368
|
const provider = recursivelyFreeze(enriched);
|
|
350
369
|
const fastPolicyAuthority = captureFastPolicyAuthority(
|
|
351
370
|
name,
|
|
@@ -499,6 +518,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
499
518
|
inMod: prov.modelInputModalities ?? null,
|
|
500
519
|
capabilities: prov.modelCapabilities ?? null,
|
|
501
520
|
re: prov.modelReasoningEfforts ?? null,
|
|
521
|
+
suppressMax: prov.modelSuppressSyntheticMax ?? null,
|
|
502
522
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
503
523
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
504
524
|
verbosity: prov.modelSupportsVerbosity ?? null,
|
|
@@ -90,6 +90,7 @@ import type { CatalogModel } from "./parsing";
|
|
|
90
90
|
import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
91
91
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
92
92
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
93
|
+
import { clampObservedModelLimits, resolveModelPolicy } from "../../providers/resolved-model-policy";
|
|
93
94
|
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
94
95
|
import type {
|
|
95
96
|
CatalogAdmissionSnapshot,
|
|
@@ -246,22 +247,6 @@ export function configuredReasoningSummarySupport(prov: OcxProviderConfig | unde
|
|
|
246
247
|
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
|
|
250
|
-
const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
|
|
251
|
-
if (explicit !== undefined) return explicit;
|
|
252
|
-
if (!prov) return undefined;
|
|
253
|
-
void name;
|
|
254
|
-
// Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered
|
|
255
|
-
// model would otherwise re-advertise a control the upstream accepts and ignores.
|
|
256
|
-
//
|
|
257
|
-
// Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
|
|
258
|
-
// registry authority up front and forbids any later registry read, so consulting the registry
|
|
259
|
-
// here made a custom-destination flight fall back to "configured" instead of serving its own
|
|
260
|
-
// discovery result (tests/codex-integration/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
|
|
261
|
-
// providers/derive.ts materializes the registry default into the config at seed/enrich time.
|
|
262
|
-
return prov.supportsVerbosity;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
250
|
export function applyProviderConfigHints(
|
|
266
251
|
name: string,
|
|
267
252
|
prov: OcxProviderConfig,
|
|
@@ -270,6 +255,15 @@ export function applyProviderConfigHints(
|
|
|
270
255
|
metadataModelIdCaseFold?: boolean,
|
|
271
256
|
effectiveAlias?: string | null,
|
|
272
257
|
): CatalogModel {
|
|
258
|
+
const staticPolicy = resolveModelPolicy({
|
|
259
|
+
providerName: name,
|
|
260
|
+
modelId: model.id,
|
|
261
|
+
provider: prov,
|
|
262
|
+
transportMatchedRegistry: false,
|
|
263
|
+
modelCapabilities: prov.modelCapabilities?.[model.id],
|
|
264
|
+
...(prov.authMode ? { effectiveAuth: { authMode: prov.authMode } } : {}),
|
|
265
|
+
...(effectiveAlias !== undefined ? { effectiveAlias } : {}),
|
|
266
|
+
});
|
|
273
267
|
const displayName = configuredModelDisplayName(prov, model.id);
|
|
274
268
|
// The alias decision is resolved once at flight admission (captureProviderGather) and threaded
|
|
275
269
|
// through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission,
|
|
@@ -279,11 +273,15 @@ export function applyProviderConfigHints(
|
|
|
279
273
|
const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null
|
|
280
274
|
? effectiveAlias
|
|
281
275
|
: model.providerAlias;
|
|
282
|
-
const configuredCap = configuredContextWindow(prov, model.id);
|
|
283
|
-
const configuredMaxInput =
|
|
276
|
+
const configuredCap = staticPolicy.model.contextWindow ?? configuredContextWindow(prov, model.id);
|
|
277
|
+
const configuredMaxInput = staticPolicy.model.maxInputTokens;
|
|
284
278
|
const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
|
|
285
279
|
const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
|
|
286
|
-
|
|
280
|
+
// The resolver owns exact capability precedence and legacy exact/colon-family/case-fold fallback.
|
|
281
|
+
// Removing an exact capability row therefore returns this projection to legacy-map inference.
|
|
282
|
+
let inputModalities = staticPolicy.model.inputModalities
|
|
283
|
+
? [...staticPolicy.model.inputModalities]
|
|
284
|
+
: undefined;
|
|
287
285
|
// The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time
|
|
288
286
|
// planning aligned. The catalog must still advertise image input — the Codex app
|
|
289
287
|
// gates attachments client-side on input_modalities, and a text-only entry would block images
|
|
@@ -296,24 +294,31 @@ export function applyProviderConfigHints(
|
|
|
296
294
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
297
295
|
}
|
|
298
296
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
297
|
+
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
|
|
298
|
+
const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
|
|
299
|
+
const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
|
|
300
|
+
const supportsVerbosity = staticPolicy.model.supportsVerbosity;
|
|
302
301
|
const fastPolicy = fastPolicyForModel(prov, model.id, name);
|
|
302
|
+
// Frozen gather providers retain the captured Fast authority. That late/captured eligibility
|
|
303
|
+
// owns catalog publication, including an explicit provider-level false.
|
|
303
304
|
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
|
|
304
305
|
const {
|
|
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 就是实际窗口。
|
|
311
313
|
const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
312
314
|
? model.contextWindow
|
|
313
315
|
: undefined;
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
316
|
+
const projectedLimits = clampObservedModelLimits(staticPolicy.model, {
|
|
317
|
+
...(discoveredWindow !== undefined ? { contextWindow: discoveredWindow } : {}),
|
|
318
|
+
...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
|
|
319
|
+
});
|
|
320
|
+
const hintedWindow = projectedLimits.contextWindow
|
|
321
|
+
?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
|
|
317
322
|
const hinted = {
|
|
318
323
|
...modelWithoutServiceTier,
|
|
319
324
|
...(displayName !== undefined ? { displayName } : {}),
|
|
@@ -321,11 +326,10 @@ export function applyProviderConfigHints(
|
|
|
321
326
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
322
327
|
...(inputModalities ? { inputModalities } : {}),
|
|
323
328
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
329
|
+
...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
|
|
324
330
|
...(configuredMaxInput !== undefined
|
|
325
331
|
? {
|
|
326
|
-
maxInputTokens:
|
|
327
|
-
? Math.min(model.maxInputTokens, configuredMaxInput)
|
|
328
|
-
: configuredMaxInput,
|
|
332
|
+
maxInputTokens: projectedLimits.maxInputTokens ?? configuredMaxInput,
|
|
329
333
|
}
|
|
330
334
|
: {}),
|
|
331
335
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
@@ -394,6 +398,31 @@ export function applyConfigHintsToCachedModels(
|
|
|
394
398
|
): CatalogModel[] {
|
|
395
399
|
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
|
|
396
400
|
}
|
|
401
|
+
|
|
402
|
+
/** Catalog slugs whose configured model must not gain a missing synthetic max rung. */
|
|
403
|
+
export function suppressedSyntheticMaxCatalogSlugs(
|
|
404
|
+
config: Pick<OcxConfig, "providers">,
|
|
405
|
+
models: readonly CatalogModel[],
|
|
406
|
+
observedEntries: readonly { slug?: unknown }[] = [],
|
|
407
|
+
): ReadonlySet<string> {
|
|
408
|
+
const slugs = new Set(models
|
|
409
|
+
.filter(model => model.suppressSyntheticMax === true)
|
|
410
|
+
.map(catalogModelSlug));
|
|
411
|
+
for (const [provider, providerConfig] of Object.entries(config.providers)) {
|
|
412
|
+
const configured = providerConfig.modelSuppressSyntheticMax ?? {};
|
|
413
|
+
const encoded = Object.fromEntries(Object.entries(configured)
|
|
414
|
+
.map(([modelId, suppress]) => [routedSlug(provider, modelId).slice(provider.length + 1), suppress]));
|
|
415
|
+
for (const [modelId, suppress] of Object.entries(configured)) {
|
|
416
|
+
if (suppress === true) slugs.add(routedSlug(provider, modelId));
|
|
417
|
+
}
|
|
418
|
+
for (const entry of observedEntries) {
|
|
419
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
420
|
+
if (!slug.startsWith(`${provider}/`)) continue;
|
|
421
|
+
if (modelRecordValue(encoded, slug.slice(provider.length + 1)) === true) slugs.add(slug);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return slugs;
|
|
425
|
+
}
|
|
397
426
|
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
398
427
|
|
|
399
428
|
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
@@ -582,10 +611,11 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
582
611
|
/**
|
|
583
612
|
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
584
613
|
*
|
|
585
|
-
* Fail closed.
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
614
|
+
* Fail closed. Any positive numeric component proves the model is paid. Calling it free requires
|
|
615
|
+
* a complete prompt/completion pair and every published pricing component to be a non-negative
|
|
616
|
+
* numeric zero; an unsupported component is "unknown" because it may describe another charge.
|
|
617
|
+
* Showing a paid model under a Free filter spends the user's money, while hiding a free one costs
|
|
618
|
+
* a click.
|
|
589
619
|
*
|
|
590
620
|
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
591
621
|
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
@@ -598,10 +628,13 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
598
628
|
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
599
629
|
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
600
630
|
if (!pricing) return "unknown";
|
|
631
|
+
const rates = Object.values(pricing).map(discoveredPricingRate);
|
|
632
|
+
if (rates.some(rate => rate !== undefined && rate > 0)) return "paid";
|
|
633
|
+
if (rates.some(rate => rate === undefined)) return "unknown";
|
|
601
634
|
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
602
635
|
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
603
636
|
if (prompt === undefined || completion === undefined) return "unknown";
|
|
604
|
-
return
|
|
637
|
+
return "free";
|
|
605
638
|
}
|
|
606
639
|
|
|
607
640
|
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 {
|
|
@@ -741,6 +803,13 @@ export function normalizeRoutedCatalogEntry(
|
|
|
741
803
|
delete entry.multi_agent_reasoning_effort;
|
|
742
804
|
delete entry.use_responses_lite;
|
|
743
805
|
delete entry.supports_websockets;
|
|
806
|
+
// Routed rows cloned from native templates must not inherit OpenAI-only experimental context
|
|
807
|
+
// delivery. Codex reads the flag as "this model accepts experimental context history" and drives
|
|
808
|
+
// its context-management cadence from it, so a third-party provider that never negotiated it gets
|
|
809
|
+
// a compact-after-every-step loop instead (observed on a routed DeepSeek row: 1,600+ compactions
|
|
810
|
+
// in a single thread). Nothing re-applies the field from provider metadata during this step, so
|
|
811
|
+
// the row leaves this normalization without it.
|
|
812
|
+
delete entry.supports_experimental_context;
|
|
744
813
|
/*
|
|
745
814
|
* Tier metadata is stripped from routed rows because a row cloned from a native template
|
|
746
815
|
* would otherwise hand a third-party provider OpenAI's tiers.
|
|
@@ -888,3 +957,19 @@ export function readNativeBaseline(catalogPath: string): Map<string, number> {
|
|
|
888
957
|
}
|
|
889
958
|
return out;
|
|
890
959
|
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Extract the pristine baseline's per-slug multi-agent pins. A bare native row that
|
|
963
|
+
* carried no pin maps to null so "baseline says unpinned" stays distinguishable
|
|
964
|
+
* from "baseline never contained this row".
|
|
965
|
+
*/
|
|
966
|
+
export function nativeMultiAgentDefaults(
|
|
967
|
+
models: readonly Readonly<Record<string, unknown>>[] | null | undefined,
|
|
968
|
+
): Map<string, string | null> {
|
|
969
|
+
const out = new Map<string, string | null>();
|
|
970
|
+
for (const entry of models ?? []) {
|
|
971
|
+
if (typeof entry.slug !== "string" || entry.slug.includes("/")) continue;
|
|
972
|
+
out.set(entry.slug, typeof entry.multi_agent_version === "string" ? entry.multi_agent_version : null);
|
|
973
|
+
}
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
clearModelCache,
|
|
12
12
|
clearProviderDiscoveryStatus,
|
|
13
13
|
captureModelCacheGeneration,
|
|
14
|
+
observeModelCacheRevision,
|
|
14
15
|
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
15
16
|
getFreshCached,
|
|
16
17
|
getStaleCached,
|
|
@@ -108,6 +109,14 @@ import { captureProviderGather, materializeCapturedHeaders } from "./gather-capt
|
|
|
108
109
|
export interface ProviderModelsResult {
|
|
109
110
|
readonly models: CatalogModel[];
|
|
110
111
|
readonly outcome: CatalogGatherProviderModelOutcome;
|
|
112
|
+
/**
|
|
113
|
+
* The provider's cache content revision as it stood when these rows were chosen.
|
|
114
|
+
*
|
|
115
|
+
* Sampled here rather than by the caller because only this point is synchronous with the
|
|
116
|
+
* choice. A caller reading it after its own await can observe a revision another flight
|
|
117
|
+
* published in between, and would then record these rows under that flight's identity.
|
|
118
|
+
*/
|
|
119
|
+
readonly contentRevision: string;
|
|
111
120
|
}
|
|
112
121
|
export const refreshingModelsAuthResolver: ModelsAuthResolver = { kind: "refreshing" };
|
|
113
122
|
|
|
@@ -145,7 +154,11 @@ export async function fetchProviderModelsWithAuth(
|
|
|
145
154
|
const observed = (
|
|
146
155
|
models: CatalogModel[],
|
|
147
156
|
state: CatalogGatherProviderModelOutcome["state"],
|
|
148
|
-
): ProviderModelsResult => ({
|
|
157
|
+
): ProviderModelsResult => ({
|
|
158
|
+
models,
|
|
159
|
+
outcome: { provider: name, state },
|
|
160
|
+
contentRevision: observeModelCacheRevision(name),
|
|
161
|
+
});
|
|
149
162
|
// Capture before any credential refresh or outbound await. OAuth account changes clear this
|
|
150
163
|
// generation, so a request started with the former account cannot later publish its result.
|
|
151
164
|
const cacheGeneration = captureModelCacheGeneration(name);
|
|
@@ -372,7 +385,11 @@ export async function fetchProviderModelsWithAuth(
|
|
|
372
385
|
...(cursorFetch ? { fetch: cursorFetch } : {}),
|
|
373
386
|
});
|
|
374
387
|
if (liveResult.ok) {
|
|
375
|
-
const available = filterCursorConfiguredModelsByLiveDiscovery(
|
|
388
|
+
const available = filterCursorConfiguredModelsByLiveDiscovery(
|
|
389
|
+
configured,
|
|
390
|
+
liveResult.models,
|
|
391
|
+
liveResult.maxModeModels ?? [],
|
|
392
|
+
);
|
|
376
393
|
const result = available.length > 0 ? available : configured;
|
|
377
394
|
// Cache the discovery-filtered roster without combo retention so a later
|
|
378
395
|
// gather can re-apply the current capture's retain set on read.
|