@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -38,10 +38,12 @@ import type { RawEntry } from "./parsing";
38
38
  import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
39
39
  import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
40
40
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
41
+ import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
41
42
  import {
42
43
  ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
43
44
  NATIVE_DAYBREAK_BLUE_MODEL,
44
45
  NATIVE_GPT6_ASTRA_MODEL,
46
+ NATIVE_RESERVE_MODEL,
45
47
  NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
46
48
  NATIVE_OPENAI_MODELS,
47
49
  SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
@@ -70,10 +72,7 @@ export {
70
72
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
71
73
  "gpt-5.3-codex-spark",
72
74
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
73
- // Preemptive leak-based registration: no shipped codex-rs catalog carries it, so without this
74
- // entry an install WITH a live catalog would drop the row that native-models.ts deliberately
75
- // ungated. Listing it here keeps the bare slug reachable so a request actually dispatches and
76
- // reports the upstream status.
75
+ // The shipped pin also backfills older installed Codex catalogs that predate Astra.
77
76
  NATIVE_GPT6_ASTRA_MODEL,
78
77
  ];
79
78
 
@@ -671,7 +670,7 @@ function isAccountBoundOpenAiNativeSlug(slug: string): boolean {
671
670
  * malformed and minimal hand-written rows. It cannot distinguish a genuine upstream observation
672
671
  * from a complete row typed by hand into `$CODEX_HOME/models_cache.json`: there is no signature,
673
672
  * source identity, or server attestation to check. A full-shape forged row is accepted, and
674
- * `observedFullShapeRowIsAccepted` in tests/native-model-toggle.test.ts pins that so nobody
673
+ * `observedFullShapeRowIsAccepted` in tests/codex-integration/native-model-toggle.test.ts pins that so nobody
675
674
  * later mistakes this predicate for a security boundary.
676
675
  *
677
676
  * That is acceptable here because the file is user-owned and written by Codex itself: anyone
@@ -700,14 +699,44 @@ function observedAccountBoundNativeSlug(entry: RawEntry): string | undefined {
700
699
  const accountBound = trustedAccountBoundNativeCatalogSlug(entry);
701
700
  const slug = accountBound ?? (typeof entry.slug === "string" ? entry.slug : "");
702
701
  if (!isAccountBoundOpenAiNativeSlug(slug)
703
- || entry.supported_in_api !== true
702
+ || (entry.supported_in_api !== true && !(slug === NATIVE_RESERVE_MODEL && entry.supported_in_api === false))
703
+ || (slug === NATIVE_RESERVE_MODEL && entry[RESERVE_METADATA_SOURCE_FIELD] !== undefined
704
+ && entry[RESERVE_METADATA_SOURCE_FIELD] !== NATIVE_RESERVE_MODEL)
704
705
  || !hasNativeCatalogRowShape(entry)
705
- || (entry.visibility !== "list" && entry[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER] !== true)) {
706
+ || (entry.visibility !== "list" && entry[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER] !== true
707
+ && !(slug === NATIVE_RESERVE_MODEL && entry.visibility === "hide"))) {
706
708
  return undefined;
707
709
  }
708
710
  return slug;
709
711
  }
710
712
 
713
+ /** Prefer a genuine bare observation; an adapted OCX row must never become native evidence. */
714
+ export function observedReserveCatalogSource(
715
+ entries: readonly RawEntry[],
716
+ mainSelectors: readonly string[],
717
+ ): RawEntry | null {
718
+ const actual = entries.filter(entry => observedAccountBoundNativeSlug(entry) === NATIVE_RESERVE_MODEL);
719
+ const bare = actual.find(entry => entry.slug === NATIVE_RESERVE_MODEL);
720
+ const qualified = actual.find(entry => entry[RESERVE_METADATA_SOURCE_FIELD] === NATIVE_RESERVE_MODEL
721
+ && typeof entry.slug === "string"
722
+ && mainSelectors.includes(entry.slug.slice(0, entry.slug.indexOf("/"))));
723
+ const selected = bare ?? qualified;
724
+ if (!selected) return null;
725
+ const source = structuredClone(selected);
726
+ if (!bare) {
727
+ const prefix = `${String(source.slug).split("/")[0]} / `;
728
+ if (typeof source.display_name === "string" && source.display_name.startsWith(prefix)) {
729
+ source.display_name = source.display_name.slice(prefix.length);
730
+ }
731
+ }
732
+ source.slug = NATIVE_RESERVE_MODEL;
733
+ delete source.opencodex_catalog_kind;
734
+ delete source[RESERVE_METADATA_SOURCE_FIELD];
735
+ delete source[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER];
736
+ delete source[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER];
737
+ return source;
738
+ }
739
+
711
740
  /**
712
741
  * Return exact, previously observed account-native rows that are not in the static release set.
713
742
  * The result is used only to carry a hidden observation across startup cache invalidation.
@@ -748,7 +777,8 @@ export function accountBoundNativeOpenAiSlugs(
748
777
  ): string[] {
749
778
  const observed = observedEntries.flatMap(entry => {
750
779
  const slug = observedAccountBoundNativeSlug(entry);
751
- return slug === undefined ? [] : [slug];
780
+ // Reserve belongs only to the Codex-specific opt-in builder, not generic native exports.
781
+ return slug === undefined || slug === NATIVE_RESERVE_MODEL ? [] : [slug];
752
782
  });
753
783
  return unique([...NATIVE_OPENAI_MODELS, ...observed]);
754
784
  }
@@ -775,7 +805,7 @@ export function accountBoundNativeOpenAiSlugsBySelector(
775
805
  );
776
806
  for (const entry of observedEntries) {
777
807
  const slug = observedAccountBoundNativeSlug(entry);
778
- if (slug === undefined || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) continue;
808
+ if (slug === undefined || slug === NATIVE_RESERVE_MODEL || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) continue;
779
809
  const generated = trustedAccountBoundNativeCatalogSlug(entry);
780
810
  const generatedSelector = generated === undefined || typeof entry.slug !== "string"
781
811
  ? undefined
@@ -1,14 +1,9 @@
1
+ /** Reserve wire identity, not a globally available native catalog registration. */
2
+ export const NATIVE_RESERVE_MODEL = "gpt-reserve";
3
+
1
4
  /** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */
2
5
  export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
3
6
 
4
- /**
5
- * Leaked Responses API identifier for the announced next-generation OpenAI model
6
- * (2026-09-03: OpenAI teased the launch on X; community probes report `gpt-6-astra` returning
7
- * the same 404 as other internal staging slugs where an arbitrary slug returns 400).
8
- * Registered preemptively so an entitled account can route it the moment it ships, before any
9
- * codex-rs catalog carries it. Unlike Daybreak it is NOT wire-normalized to a serving id —
10
- * the leaked slug IS the wire id.
11
- */
12
7
  /**
13
8
  * SHIPPED as of 2026-09-03: openai/codex `ed391d4dd` (#42607, bundled model catalog) and
14
9
  * `1f7b99922` (#42619, Amazon Bedrock catalogs). The registration is no longer speculative —
@@ -27,11 +22,32 @@ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
27
22
  */
28
23
  export const NATIVE_GPT6_ASTRA_MODEL = "gpt-6-astra";
29
24
 
30
- /** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
25
+ /**
26
+ * Native ChatGPT/Codex ids whose availability is proven per authenticated account.
27
+ *
28
+ * Membership is expensive: it hides the row from the catalog, `/v1/models`, the dashboard and
29
+ * the desktop projection until an authenticated `/models` roster confirms it, AND it makes
30
+ * `auth-context.ts` refuse the request before it is sent. Both halves fail closed on ABSENCE of
31
+ * evidence, not on a denial.
32
+ *
33
+ * The flagship models are deliberately NOT here (owner decision, 2026-09-04). #3442 made
34
+ * discovery ask upstream under an adequate client version, which guarantees the QUESTION is
35
+ * fair but cannot guarantee an ANSWER: an unconfirmed account, a timed-out fetch, or a shard
36
+ * that has not caught up all produce the same silent disappearance, and a model vanishing from
37
+ * the picker reads as "opencodex lost my model" rather than "upstream did not confirm it".
38
+ * Listing them unconditionally means the request dispatches and the user sees the real upstream
39
+ * status. `disabledModels` remains the visibility lever.
40
+ *
41
+ * The cost, accepted knowingly: Pool routing no longer prefers an account that owns the model,
42
+ * so a multi-account user may take one upstream 400 and one alternate retry where they used to
43
+ * be routed straight to the owner. Nothing unsafe — each account still sends its own credential
44
+ * — and `gpt-6-astra` has shipped this way since 6f634eddc.
45
+ *
46
+ * `gpt-daybreak-blue-latest` stays gated. It has no shipped catalog row anywhere, so absence is
47
+ * the only signal that exists for it, and the ungating decision was scoped to the flagships.
48
+ * Evidence: devlog/_plan/260904_flagship_native_always_visible/.
49
+ */
31
50
  export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
32
- "gpt-5.6-sol",
33
- "gpt-5.6-terra",
34
- "gpt-5.6-luna",
35
51
  NATIVE_DAYBREAK_BLUE_MODEL,
36
52
  ]);
37
53
 
@@ -144,3 +160,28 @@ export const NATIVE_OPENAI_MODELS = [
144
160
  ];
145
161
 
146
162
  export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
163
+
164
+ /**
165
+ * Natives that retain the physical main account as a read-free sentinel during a native-main
166
+ * drain, instead of reading as unavailable and letting the subagent fallback chain advance.
167
+ *
168
+ * This used to be spelled `ACCOUNT_GATED_NATIVE_OPENAI_MODELS`, which was never what it meant:
169
+ * the sentinel protects the atomic main claim so a routed fallback cannot bypass it, and that
170
+ * has nothing to do with entitlement. The two sets were identical in practice, so the accident
171
+ * went unnoticed until the flagships were ungated (2026-09-04) and the predicate would have
172
+ * flipped false — letting a drain silently rewrite the operator's configured subagent model.
173
+ *
174
+ * It is an explicit list rather than `SUPPORTED_NATIVE_OPENAI_SLUGS`, which would have widened
175
+ * the sentinel to `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini` and `gpt-5.3-codex-spark` as well. Those
176
+ * models were never covered, and widening would turn "fell back and answered" into a
177
+ * maintenance error for the most commonly configured fallback slug in the repo. Membership is
178
+ * the set the drain behaviour was actually reasoned about: the account-gated natives plus the
179
+ * flagships that just left that set.
180
+ */
181
+ export const NATIVE_MAIN_DRAIN_SENTINEL_MODELS: ReadonlySet<string> = new Set([
182
+ ...ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
183
+ "gpt-5.6-sol",
184
+ "gpt-5.6-terra",
185
+ "gpt-5.6-luna",
186
+ NATIVE_GPT6_ASTRA_MODEL,
187
+ ]);
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
4
4
  import { delimiter, dirname, join, resolve } from "node:path";
5
- import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
5
+ import { atomicWriteFile, expandUserPath, getConfigDir, loadConfig, ultraFastTierEnabled, websocketsEnabled } from "../../config";
6
6
  import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
7
7
  import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
8
8
  import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
@@ -35,6 +35,7 @@ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREA
35
35
  import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
36
36
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
37
37
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
38
+ import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
38
39
 
39
40
  export function legacyCatalogBackupPath(): string {
40
41
  return join(getConfigDir(), "catalog-backup.json");
@@ -95,6 +96,8 @@ export const CODEX_PROVIDER_MODEL_CATALOG_KIND = "provider-model-v1";
95
96
  export interface CatalogModel {
96
97
  id: string;
97
98
  provider: string;
99
+ /** Canonical or configured short alias for the provider segment. */
100
+ providerAlias?: string | null;
98
101
  /** Public Codex-facing slug override (used by combo aliases). */
99
102
  alias?: string;
100
103
  /** Explicit combo takeover of a bare OpenAI-native catalog id. */
@@ -303,7 +306,98 @@ const NO_FAST_TIER_NATIVE_SLUGS = new Set([
303
306
  "gpt-5.3-codex-spark",
304
307
  ]);
305
308
 
309
+ /** Does this row already carry an `ultrafast` tier the operator put there themselves? */
310
+ /**
311
+ * Read the opt-in from the live config, ONCE per catalog build.
312
+ *
313
+ * `deriveEntry` and its call sites are pure `RawEntry -> RawEntry` transforms with no
314
+ * config parameter, so the flag is resolved here rather than threaded through all of them.
315
+ * It is memoized because `normalizeRoutedCatalogEntry` runs per entry in a sync loop, and
316
+ * `loadConfig()` chmods the config dir, hardens three secrets, reads the file and runs a
317
+ * full Zod parse — doing that once per catalog row would be a real cost for one boolean.
318
+ * Callers holding a config still pass `opts.ultraFastTier` explicitly, which bypasses this
319
+ * entirely and is what the tests do. A read failure means OFF, matching `.catch(false)`.
320
+ */
321
+ let ultraFastOptInCache: { value: boolean; at: number } | null = null;
322
+ const ULTRA_FAST_OPT_IN_TTL_MS = 5_000;
323
+
324
+ function ultraFastTierOptIn(): boolean {
325
+ const now = Date.now();
326
+ if (ultraFastOptInCache && now - ultraFastOptInCache.at < ULTRA_FAST_OPT_IN_TTL_MS) {
327
+ return ultraFastOptInCache.value;
328
+ }
329
+ let value = false;
330
+ try {
331
+ value = ultraFastTierEnabled(loadConfig());
332
+ } catch {
333
+ value = false;
334
+ }
335
+ ultraFastOptInCache = { value, at: now };
336
+ return value;
337
+ }
338
+
339
+ /** Test seam: drop the memoized opt-in so a config change is observed immediately. */
340
+ export function resetUltraFastTierOptInCache(): void {
341
+ ultraFastOptInCache = null;
342
+ }
343
+
344
+ function entryDeclaresUltraFast(entry: RawEntry): boolean {
345
+ const tiers = entry.service_tiers;
346
+ const declaredInTiers = Array.isArray(tiers) && tiers.some(tier => (
347
+ !!tier && typeof tier === "object" && "id" in tier
348
+ && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast"
349
+ ));
350
+ const speeds = entry.additional_speed_tiers;
351
+ const declaredInSpeeds = Array.isArray(speeds) && speeds.some(speed => (
352
+ typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast"
353
+ ));
354
+ return declaredInTiers || declaredInSpeeds;
355
+ }
356
+
357
+ /**
358
+ * Keep the operator's `ultrafast` and drop everything else.
359
+ *
360
+ * A routed row must not inherit the native template's `priority` tier, which is the whole
361
+ * reason this strip exists. Preserving the supplied tier without this narrowing would
362
+ * smuggle Fast onto third-party providers under an unrelated flag.
363
+ */
364
+ function retainOnlyUltraFastTier(entry: RawEntry): void {
365
+ const tiers = entry.service_tiers;
366
+ const keptTiers = Array.isArray(tiers)
367
+ ? tiers.filter(tier => (
368
+ !!tier && typeof tier === "object" && "id" in tier
369
+ && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast"
370
+ ))
371
+ : [];
372
+ if (keptTiers.length > 0) entry.service_tiers = keptTiers;
373
+ else delete entry.service_tiers;
374
+
375
+ const speeds = entry.additional_speed_tiers;
376
+ const keptSpeeds = Array.isArray(speeds)
377
+ ? speeds.filter(speed => typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast")
378
+ : [];
379
+ if (keptSpeeds.length > 0) entry.additional_speed_tiers = keptSpeeds;
380
+ else delete entry.additional_speed_tiers;
381
+
382
+ // A default of `priority` on a row that now only offers ultrafast would name a tier the
383
+ // row no longer carries.
384
+ if (String(entry.service_tier ?? "").trim().toLowerCase() !== "ultrafast") delete entry.service_tier;
385
+ if (String(entry.default_service_tier ?? "").trim().toLowerCase() !== "ultrafast") {
386
+ delete entry.default_service_tier;
387
+ }
388
+ }
389
+
306
390
  export function normalizeServiceTiers(entry: RawEntry): RawEntry {
391
+ // Repair only the old built-in Astra speed copy on persisted native/account rows.
392
+ // A custom description (and every other field) remains user-owned.
393
+ const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry) ?? entry.slug;
394
+ if (nativeSlug === NATIVE_GPT6_ASTRA_MODEL && Array.isArray(entry.service_tiers)) {
395
+ entry.service_tiers = entry.service_tiers.map(tier =>
396
+ tier && typeof tier === "object" && tier.id === "priority"
397
+ && tier.description === "1.5x speed, increased usage"
398
+ ? { ...tier, description: "2x speed, increased usage" } : tier,
399
+ );
400
+ }
307
401
  // Strip service tiers for models that do not actually support the Fast tier.
308
402
  if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) {
309
403
  delete entry.service_tier;
@@ -502,6 +596,8 @@ export function ensureStrictCatalogFields(
502
596
  export type MultiAgentMode = "v1" | "default" | "v2";
503
597
 
504
598
  export interface MultiAgentModeOptions {
599
+ /** Caller-owned source metadata already defines the default for these projected rows. */
600
+ preserveDefaultMultiAgentVersion?: (entry: RawEntry) => boolean;
505
601
  /**
506
602
  * When the catalog is in v2 mode, stamp ChatGPT-native rows as v1 instead.
507
603
  * Routed parents get v2 (plaintext child tasks). Native Sol/Terra stay on v1
@@ -577,6 +673,7 @@ export function applyMultiAgentMode(
577
673
  // Restore upstream defaults: clear any stale forced multi_agent_version and
578
674
  // re-apply upstream pins from the snapshot for native entries that have one.
579
675
  for (const entry of entries) {
676
+ if (options.preserveDefaultMultiAgentVersion?.(entry)) continue;
580
677
  const slug = typeof entry.slug === "string" ? entry.slug : "";
581
678
  const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
582
679
  const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`)
@@ -612,17 +709,34 @@ export function normalizeRoutedCatalogEntry(
612
709
  entry: RawEntry,
613
710
  parallelToolCalls = false,
614
711
  toolMode?: "code_mode_only" | "shell" | string,
712
+ opts?: { ultraFastTier?: boolean },
615
713
  ): RawEntry {
616
714
  delete entry.model_messages;
617
715
  delete entry.tool_mode;
618
716
  applyRoutedCodexToolMode(entry, toolMode);
619
717
  delete entry.multi_agent_version;
718
+ delete entry.multi_agent_reasoning_effort;
620
719
  delete entry.use_responses_lite;
621
720
  delete entry.supports_websockets;
622
- delete entry.additional_speed_tiers;
623
- delete entry.service_tier;
624
- delete entry.service_tiers;
625
- delete entry.default_service_tier;
721
+ /*
722
+ * Tier metadata is stripped from routed rows because a row cloned from a native template
723
+ * would otherwise hand a third-party provider OpenAI's tiers.
724
+ *
725
+ * The opt-in carves out exactly one case: an `ultrafast` the OPERATOR put in their own
726
+ * catalog. #3429's reporter added it by hand and watched a regeneration delete it every
727
+ * time. Preserving what they wrote is not the same as advertising a tier — upstream
728
+ * publishes only `priority`, and synthesizing an ultrafast row is what PR #2994 was
729
+ * closed for. So this keeps a supplied tier and still never invents one.
730
+ */
731
+ const keepUltraFast = (opts?.ultraFastTier ?? ultraFastTierOptIn()) && entryDeclaresUltraFast(entry);
732
+ if (!keepUltraFast) {
733
+ delete entry.additional_speed_tiers;
734
+ delete entry.service_tier;
735
+ delete entry.service_tiers;
736
+ delete entry.default_service_tier;
737
+ } else {
738
+ retainOnlyUltraFastTier(entry);
739
+ }
626
740
  // Routed rows cloned from native templates must not inherit OpenAI-only summary delivery.
627
741
  // Explicit provider/model metadata is re-applied after this normalization step.
628
742
  delete entry.supports_reasoning_summaries;