@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.
Files changed (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. 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. max only: 5.6 exact ladders (luna: no ultra) stay intact.
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 && !levels.some(level => level.effort === "max")) {
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(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning || preservePinnedNativeCustomReasoning(model));
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): every reasoning-capable model advertises `max`
232
- // even when the provider ladder stops lower — subagent spawns pass `max` DIRECTLY
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
- : efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high"
262
- // Sentinels never become the implicit default when real rungs are declared.
263
- : efforts.find(effort => effort !== "none" && effort !== "minimal") ?? efforts[0];
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 { applyRegistryCapabilitySeedFill, modelCapabilities, modelInputModalities } from "./model-hints";
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 = configuredMaxInputTokens(prov, model.id);
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
- let inputModalities = configuredInputModalities(prov, model.id);
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 defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
300
- const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
301
- const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
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 hintedWindow = discoveredWindow !== undefined
315
- ? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
316
- : (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
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: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
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. Only a complete pair of non-negative numeric rates classifies at all; a missing,
586
- * one-sided, non-numeric, or negative rate is "unknown" and therefore excluded from a free-only
587
- * filter. Showing a paid model under a Free filter spends the user's money, while hiding a free
588
- * one costs a click.
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 prompt === 0 && completion === 0 ? "free" : "paid";
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
- /** Resolve the configured catalog without consulting ambient CODEX_HOME again. */
259
- export function readCodexCatalogPathForHome(codexHome: string): string {
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
- : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
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 => ({ models, outcome: { provider: name, state } });
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(configured, liveResult.models);
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.