@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,981 @@
1
+ import { CODEX_REASONING_LEVELS } from "../../reasoning-effort";
2
+ import { clearModelCache } from "../model-cache";
3
+ import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
4
+ import { COMBO_NAMESPACE } from "../../combos";
5
+ import {
6
+ CODEX_CUSTOM_MODEL_CATALOG_KIND,
7
+ CODEX_PROVIDER_MODEL_CATALOG_KIND,
8
+ applyMultiAgentMode,
9
+ applyNativeOpenAiContextOverride,
10
+ catalogModelSlug,
11
+ ensureStrictCatalogFields,
12
+ isRoutedModelCompatibilityExcluded,
13
+ normalizeServiceTiers,
14
+ } from "./parsing";
15
+ import type { CatalogModel, MultiAgentMode, RawEntry } from "./parsing";
16
+ import {
17
+ CODEX_NATIVE_ALIAS_CATALOG_KIND,
18
+ NATIVE_OPENAI_MODELS,
19
+ SUPPORTED_NATIVE_OPENAI_SLUGS,
20
+ applyNativeVisibility,
21
+ isNativeAliasCatalogEntry,
22
+ isUnsupportedOpenAiNativeSlug,
23
+ shouldUpgradeToUpstreamEntry,
24
+ upstreamNativeEntry,
25
+ type NativeContextLimitsInput,
26
+ } from "./metadata";
27
+ import { resetBundledCatalogCacheForTests } from "./bundled";
28
+ import { isMultiAgentV2Enabled } from "../features";
29
+ import { ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
30
+ import { clearGatherRoutedModelsInflight, lastDropWarnSignature } from "./provider-fetch";
31
+ import {
32
+ accountSelectorShadowCollisionWarnings,
33
+ clearLastComboCatalogOmissions,
34
+ comboCatalogWarningSignatures,
35
+ comboMasqueradeCollisionWarnings,
36
+ comboUnrestorableShadowWarnings,
37
+ openAiApiCollisionWarnings,
38
+ resolveSlugAliasCollisions,
39
+ slugAliasCollisionWarnings,
40
+ warnAccountSelectorShadowedProviderOnce,
41
+ warnComboMasqueradeCollisionOnce,
42
+ warnComboUnrestorableShadowOnce,
43
+ } from "./aggregation";
44
+ import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug } from "./account-models";
45
+ import { NATIVE_RESERVE_MODEL } from "./native-models";
46
+ import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
47
+ import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
48
+ import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
49
+
50
+ export interface ObservedCatalogEntryBuildInput {
51
+ readonly template: RawEntry | null;
52
+ readonly gptSlugs: readonly string[];
53
+ readonly goModels: readonly CatalogModel[];
54
+ readonly featured?: readonly string[];
55
+ /** Optional full picker ordering (config.modelPickerOrder); orders non-featured rows. */
56
+ readonly modelPickerOrder?: readonly string[];
57
+ readonly wsEnabled: boolean;
58
+ readonly multiAgentMode: MultiAgentMode;
59
+ readonly exactComboSlugs: ReadonlySet<string>;
60
+ readonly accountSelectors: readonly string[];
61
+ readonly suppressedBareNativeSlugs: ReadonlySet<string>;
62
+ readonly disabledNativeAccountSlugs: ReadonlySet<string>;
63
+ readonly multiAgentV2Enabled: boolean;
64
+ readonly keepNativeChatGptOnV1?: boolean;
65
+ readonly openaiContextCap?: NativeContextLimitsInput;
66
+ /** Additional native ids to clone under account selectors, without creating bare rows. */
67
+ readonly accountNativeSlugs?: readonly string[];
68
+ /** Per-selector account ids; unknown observations must not be copied to unrelated accounts. */
69
+ readonly accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>;
70
+ /** Codex-only manual selector metadata; deliberately independent of live permission. */
71
+ readonly reserve?: ReserveCatalogProjection;
72
+ }
73
+
74
+ /** Build entries with the process-observed Codex feature state. */
75
+ export function buildCatalogEntries(
76
+ template: RawEntry | null,
77
+ gptSlugs: string[],
78
+ goModels: CatalogModel[],
79
+ featured?: string[],
80
+ wsEnabled = false,
81
+ multiAgentMode: MultiAgentMode = "default",
82
+ exactComboSlugs: ReadonlySet<string> = new Set(),
83
+ accountSelectors: readonly string[] = [],
84
+ suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
85
+ disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
86
+ contextCap?: NativeContextLimitsInput,
87
+ accountNativeSlugs?: readonly string[],
88
+ accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
89
+ keepNativeChatGptOnV1 = false,
90
+ modelPickerOrder: readonly string[] = [],
91
+ ): RawEntry[] {
92
+ const entries = buildCatalogEntriesFromObservedState({
93
+ template,
94
+ gptSlugs,
95
+ goModels,
96
+ featured,
97
+ modelPickerOrder,
98
+ wsEnabled,
99
+ multiAgentMode,
100
+ exactComboSlugs,
101
+ accountSelectors,
102
+ suppressedBareNativeSlugs,
103
+ disabledNativeAccountSlugs,
104
+ multiAgentV2Enabled: isMultiAgentV2Enabled(),
105
+ keepNativeChatGptOnV1,
106
+ openaiContextCap: contextCap,
107
+ accountNativeSlugs,
108
+ accountNativeSlugsBySelector,
109
+ });
110
+ applyFullModelPickerOrder(entries, modelPickerOrder);
111
+ return entries;
112
+ }
113
+
114
+ /** Build entries solely from caller-observed inputs, with no feature-state filesystem read. */
115
+ export function buildCatalogEntriesFromObservedState({
116
+ template,
117
+ gptSlugs,
118
+ goModels,
119
+ featured,
120
+ modelPickerOrder,
121
+ wsEnabled,
122
+ multiAgentMode,
123
+ exactComboSlugs,
124
+ accountSelectors,
125
+ suppressedBareNativeSlugs,
126
+ disabledNativeAccountSlugs,
127
+ multiAgentV2Enabled,
128
+ keepNativeChatGptOnV1,
129
+ openaiContextCap,
130
+ accountNativeSlugs,
131
+ accountNativeSlugsBySelector,
132
+ reserve,
133
+ }: ObservedCatalogEntryBuildInput): RawEntry[] {
134
+ // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
135
+ // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
136
+ // ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
137
+ // it sorts to the front. This works for native gpt slugs AND routed slugs alike.
138
+ const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
139
+ const priorityStride = Math.max(accountSelectors.length, 1);
140
+ // Optional full picker order (#1649). Independent of the 5-slot spawn_agent cap: it only
141
+ // rewrites the Codex-visible display `priority` of listed non-featured routed rows so a >5
142
+ // catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
143
+ // modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
144
+ // before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
145
+ // this display reorder does not change OpenCodex's guidance candidate calculation.
146
+ const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
147
+ const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
148
+ const pickerOrderActive = pickerOrder.length > 0;
149
+ // The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
150
+ // same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
151
+ // band. OpenCodex guidance membership does not depend on this — see SPAWN_PRIORITY_FIELD.
152
+ /**
153
+ * Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
154
+ * slugs sort in declared order within the high picker-order display tier
155
+ * (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
156
+ * the row's natural priority in SPAWN_PRIORITY_FIELD for OpenCodex's unchanged guidance window.
157
+ * Returns undefined when the feature is off or the row is not listed, so those rows
158
+ * keep their original assignment (default 5 / account 1_000+) untouched.
159
+ *
160
+ * Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
161
+ * below). Native passthrough rows and account-qualified native rows keep their own priority
162
+ * logic and are intentionally not reordered in this legacy builder pass. The final merge can
163
+ * apply complete ordering when the configured list includes a bare id.
164
+ */
165
+ const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
166
+ if (!pickerOrderActive) return undefined;
167
+ const hit = pickerOrderRank.get(slug) ?? (altSlug !== undefined ? pickerOrderRank.get(altSlug) : undefined);
168
+ if (hit === undefined) return undefined;
169
+ return PICKER_ORDER_PRIORITY_BASE + hit * priorityStride;
170
+ };
171
+ const out: RawEntry[] = [];
172
+ const nativeEntries: RawEntry[] = [];
173
+ const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
174
+ const emittedNativeAliases = new Set<CatalogModel>();
175
+ const emittedNativeAliasSlugs = new Set<string>();
176
+ const nativeAliasesBySlug = new Map<string, CatalogModel>();
177
+ for (const model of goModels) {
178
+ if (model.provider !== COMBO_NAMESPACE
179
+ || model.nativeAlias !== true
180
+ || typeof model.alias !== "string"
181
+ || model.alias.includes("/")) continue;
182
+ if (nativeAliasesBySlug.has(model.alias)) {
183
+ collisionSkipped.add(model);
184
+ if (!slugAliasCollisionWarnings.has(model.alias)) {
185
+ slugAliasCollisionWarnings.add(model.alias);
186
+ console.warn(
187
+ `[opencodex] native combo alias collision on "${model.alias}": keeping the first configured combo and omitting later duplicates from the catalog.`,
188
+ );
189
+ }
190
+ continue;
191
+ }
192
+ nativeAliasesBySlug.set(model.alias, model);
193
+ }
194
+ const comboPublicSlugs = new Set(goModels
195
+ .filter(model => model.provider === COMBO_NAMESPACE)
196
+ .map(catalogModelSlug));
197
+ for (const slug of gptSlugs) {
198
+ const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
199
+ if (rank.has(slug)) native.priority = rank.get(slug)!;
200
+ nativeEntries.push(native);
201
+ const nativeAlias = nativeAliasesBySlug.get(slug);
202
+ if (!nativeAlias || collisionSkipped.has(nativeAlias)) {
203
+ if (!suppressedBareNativeSlugs.has(slug)) out.push(native);
204
+ continue;
205
+ }
206
+ const routed = deriveEntry(
207
+ template,
208
+ slug,
209
+ `Routed via opencodex → ${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
210
+ 5,
211
+ nativeAlias,
212
+ exactComboSlugs,
213
+ );
214
+ routed.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
215
+ const rankHit = rank.get(slug) ?? rank.get(`${nativeAlias.provider}/${nativeAlias.id}`);
216
+ if (rankHit !== undefined) routed.priority = rankHit * priorityStride;
217
+ else if (accountSelectors.length > 0) routed.priority = 1_000 + (typeof routed.priority === "number" ? routed.priority : 5);
218
+ out.push(routed);
219
+ emittedNativeAliases.add(nativeAlias);
220
+ emittedNativeAliasSlugs.add(slug);
221
+ }
222
+ const nativeEntriesBySlug = new Map(nativeEntries.map(entry => [String(entry.slug), entry] as const));
223
+ for (const [selectorIndex, selector] of accountSelectors.entries()) {
224
+ const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
225
+ ?? accountNativeSlugs
226
+ ?? gptSlugs;
227
+ const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
228
+ nativeEntriesBySlug.get(slug)
229
+ ?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
230
+ ));
231
+ if (reserve?.mainSelectors.includes(selector)) accountNativeEntries.push(reserve.source);
232
+ for (const [nativeIndex, native] of accountNativeEntries.entries()) {
233
+ const nativeSlug = String(native.slug);
234
+ if (disabledNativeAccountSlugs.has(nativeSlug)) continue;
235
+ const e = JSON.parse(JSON.stringify(native)) as RawEntry;
236
+ const catalogSlug = `${selector}/${nativeSlug}`;
237
+ if (nativeSlug === NATIVE_RESERVE_MODEL && disabledNativeAccountSlugs.has(catalogSlug)) continue;
238
+ e.slug = catalogSlug;
239
+ e.display_name = accountBoundNativeDisplayName(selector, native);
240
+ // Codex ignores this OpenCodex extension; preserve the native comp_hash unchanged.
241
+ e.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
242
+ const exactRank = rank.get(catalogSlug);
243
+ // A bare featured id belongs to the compatibility combo once shadowed. Exact
244
+ // account-qualified picks still rank normally, but the account clone must not
245
+ // inherit the bare alias rank and consume another top spawn_agent slot.
246
+ const inheritedRank = emittedNativeAliasSlugs.has(nativeSlug) ? undefined : rank.get(nativeSlug);
247
+ const featuredRank = exactRank ?? inheritedRank;
248
+ e.priority = featuredRank !== undefined
249
+ ? featuredRank * priorityStride + selectorIndex
250
+ : ((featured?.length ?? 0) + nativeIndex) * accountSelectors.length + selectorIndex;
251
+ e.visibility = "list";
252
+ out.push(e);
253
+ }
254
+ }
255
+ for (const m of goModels) {
256
+ if (collisionSkipped.has(m) || emittedNativeAliases.has(m)) continue;
257
+ const slug = catalogModelSlug(m);
258
+ if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
259
+ warnComboMasqueradeCollisionOnce(slug);
260
+ continue;
261
+ }
262
+ // Provider rows use the one-slash slug codec; combo aliases intentionally override that
263
+ // public slug and may be bare.
264
+ const e = deriveEntry(
265
+ template,
266
+ slug,
267
+ `Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
268
+ 5,
269
+ m,
270
+ exactComboSlugs,
271
+ );
272
+ if (m.provider === COMBO_NAMESPACE && m.nativeAlias === true && !slug.includes("/")) {
273
+ e.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
274
+ }
275
+ // Featured picks may be stored raw (legacy) or encoded — honor both.
276
+ const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
277
+ // Natural priority: what the row would get WITHOUT modelPickerOrder. This is the value the
278
+ // spawn_agent candidate window is derived from (see effectiveSubagentRoster), so it must never
279
+ // move when modelPickerOrder reorders the picker.
280
+ if (rankHit !== undefined) e.priority = rankHit * priorityStride;
281
+ else if (accountSelectors.length > 0) {
282
+ // Keep the generated account rows together in Codex's priority-sorted flat picker.
283
+ e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
284
+ }
285
+ // The legacy routed-only builder pass keeps featured ranks and records natural priority
286
+ // before changing non-featured display priority. The final complete-order pass may move
287
+ // featured display rows too; OpenCodex guidance continues to use their natural ranks.
288
+ if (rankHit === undefined) {
289
+ const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
290
+ if (pickerPriority !== undefined) {
291
+ e[SPAWN_PRIORITY_FIELD] = typeof e.priority === "number" ? e.priority : 5;
292
+ e.priority = pickerPriority;
293
+ }
294
+ }
295
+ out.push(e);
296
+ }
297
+ // Central capability override (phase 120.4): the advertised flag must match the implemented WS
298
+ // endpoint. Overrides both the routed strip (normalizeRoutedCatalogEntry) and any native template
299
+ // leak (deriveEntry clones the template as-is for native slugs).
300
+ for (const entry of out) {
301
+ if (wsEnabled) entry.supports_websockets = true;
302
+ else {
303
+ delete entry.supports_websockets;
304
+ // Snapshot-backed native entries carry prefer_websockets: never advertise a preference
305
+ // for an endpoint ocx has disabled.
306
+ delete entry.prefer_websockets;
307
+ }
308
+ }
309
+ return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
310
+ keepNativeChatGptOnV1,
311
+ preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
312
+ });
313
+ }
314
+
315
+ export function resetCatalogRuntimeStateForTests(): void {
316
+ resetBundledCatalogCacheForTests();
317
+ lastDropWarnSignature.clear();
318
+ openAiApiCollisionWarnings.clear();
319
+ comboCatalogWarningSignatures.clear();
320
+ slugAliasCollisionWarnings.clear();
321
+ comboMasqueradeCollisionWarnings.clear();
322
+ comboUnrestorableShadowWarnings.clear();
323
+ accountSelectorShadowCollisionWarnings.clear();
324
+ clearLastComboCatalogOmissions();
325
+ clearModelCache(undefined, "eviction");
326
+ clearGatherRoutedModelsInflight();
327
+ }
328
+
329
+ export function orderForSubagents(goModels: CatalogModel[], featured?: string[]): CatalogModel[] {
330
+ if (!featured || featured.length === 0) return goModels;
331
+ const rank = new Map(featured.map((id, i) => [id, i]));
332
+ // Featured picks may be stored raw (legacy) or encoded — match both forms.
333
+ const rankOf = (m: CatalogModel) =>
334
+ (m.alias ? rank.get(m.alias) : undefined)
335
+ ?? rank.get(`${m.provider}/${m.id}`)
336
+ ?? rank.get(routedSlug(m.provider, m.id))
337
+ ?? Number.MAX_SAFE_INTEGER;
338
+ return [...goModels].sort((a, b) => {
339
+ return rankOf(a) - rankOf(b);
340
+ });
341
+ }
342
+
343
+ /** Routed discovery projection; native groups and alias ownership belong to the caller. */
344
+ export function orderForModelPicker(
345
+ models: readonly CatalogModel[],
346
+ order: readonly string[] = [],
347
+ featured: readonly string[] = [],
348
+ ): CatalogModel[] {
349
+ const pickerOrder = normalizeModelPickerOrder(order);
350
+ if (pickerOrder.length === 0) return [...models];
351
+ const pickerRank = modelPickerRank(pickerOrder);
352
+ const featuredRank = modelPickerRank(featured);
353
+ const complete = pickerOrder.some(slug => !slug.includes("/"));
354
+ const rank = (model: CatalogModel): number => {
355
+ const slug = catalogModelSlug(model);
356
+ const featuredIndex = featuredRank(slug) ?? featuredRank(`${model.provider}/${model.id}`);
357
+ const natural = featuredIndex ?? 5;
358
+ const index = pickerRank(slug) ?? pickerRank(`${model.provider}/${model.id}`);
359
+ if (complete) return index ?? pickerOrder.length + natural;
360
+ // Preserve the legacy featured/alias bands, including unlisted rows before listed rows.
361
+ if (featuredIndex !== undefined || model.nativeAlias === true) return natural;
362
+ return index === undefined ? natural : PICKER_ORDER_PRIORITY_BASE + index;
363
+ };
364
+ return [...models].sort((a, b) => rank(a) - rank(b));
365
+ }
366
+
367
+ /**
368
+ * True when an existing catalog row was authored by OpenCodex routing (#855).
369
+ * Every generated routed row — current full-slug form, the June–July 2026
370
+ * provider-name form, and legacy combo aliases — carries the stable
371
+ * description prefix `Routed via opencodex → `; foreign rows from Cursor or
372
+ * user tooling do not. `owned_by` cannot serve as the signal (upstream
373
+ * ownership), and `comp_hash` defaults to "opencodex" for every normalized
374
+ * row.
375
+ */
376
+ function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
377
+ if (isNativeAliasCatalogEntry(entry)) return true;
378
+ const desc = typeof entry.description === "string" ? entry.description : "";
379
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
380
+ return slug.includes("/") && desc.startsWith("Routed via opencodex → ");
381
+ }
382
+
383
+ function recoverableNativeSlug(entry: RawEntry): string | null {
384
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
385
+ return SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
386
+ && !isNativeAliasCatalogEntry(entry)
387
+ && entry.owned_by !== COMBO_NAMESPACE
388
+ ? slug
389
+ : null;
390
+ }
391
+
392
+ /** Undo our display overlay before native metadata normalization and template reuse. */
393
+ function restoreNativeDisplayName(entry: RawEntry): RawEntry {
394
+ const saved = entry.opencodex_native_display_name;
395
+ delete entry.opencodex_native_display_name;
396
+ if (saved && typeof saved === "object" && !Array.isArray(saved)) {
397
+ const label = saved as Record<string, unknown>;
398
+ if (recoverableNativeSlug(entry) === label.slug
399
+ && typeof label.original === "string" && entry.display_name === label.applied) {
400
+ entry.display_name = label.original;
401
+ }
402
+ }
403
+ return entry;
404
+ }
405
+
406
+ /** Append missing supported native rows from trusted catalog sources only. */
407
+ export function mergeCatalogModelsWithNativeRecovery(
408
+ primaryCatalogModels: readonly RawEntry[],
409
+ nativeRecoverySources: readonly (readonly RawEntry[])[],
410
+ ): RawEntry[] {
411
+ const merged = [...primaryCatalogModels];
412
+ const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
413
+ const slug = recoverableNativeSlug(entry);
414
+ return slug === null ? [] : [slug];
415
+ }));
416
+ for (const source of nativeRecoverySources) {
417
+ for (const entry of source) {
418
+ const slug = recoverableNativeSlug(entry);
419
+ if (slug === null || recoveredNativeSlugs.has(slug)) continue;
420
+ merged.push(structuredClone(entry) as RawEntry);
421
+ recoveredNativeSlugs.add(slug);
422
+ }
423
+ }
424
+ return merged;
425
+ }
426
+
427
+ export interface ObservedCatalogMergePolicy {
428
+ /** Required observed/fixed set; the core merge never consults ambient catalog state. */
429
+ readonly nativeBackfillSlugs: readonly string[];
430
+ /** Whether unsupported OpenAI-family bare rows survive the merge. */
431
+ readonly unsupportedNativeEntries: "preserve" | "drop";
432
+ /** Whether merge-policy collision/preservation warnings belong to this caller's flow. */
433
+ readonly warningPolicy: "emit" | "suppress";
434
+ }
435
+
436
+ /** Content policy shared by every writer of the canonical Codex model catalog. */
437
+ export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
438
+ Pick<ObservedCatalogMergePolicy, "nativeBackfillSlugs" | "unsupportedNativeEntries">
439
+ > = Object.freeze({
440
+ nativeBackfillSlugs: Object.freeze([...NATIVE_OPENAI_MODELS]),
441
+ unsupportedNativeEntries: "drop",
442
+ });
443
+
444
+ function normalizeModelPickerOrder(order: unknown): string[] {
445
+ return Array.isArray(order)
446
+ ? order.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
447
+ : [];
448
+ }
449
+
450
+ /** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */
451
+ function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined {
452
+ const exact = new Map(order.map((slug, index) => [slug, index]));
453
+ const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index]));
454
+ return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug));
455
+ }
456
+
457
+ /** Complete display ordering retains natural ranks for OpenCodex's separate guidance projection. */
458
+ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void {
459
+ const pickerOrder = normalizeModelPickerOrder(order);
460
+ if (!pickerOrder.some(slug => !slug.includes("/"))) return;
461
+ const rankOf = modelPickerRank(pickerOrder);
462
+ for (const entry of entries) {
463
+ const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
464
+ entry[SPAWN_PRIORITY_FIELD] = natural;
465
+ entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
466
+ }
467
+ }
468
+
469
+ export interface ObservedCatalogMergeInput {
470
+ readonly catalogModels: readonly RawEntry[];
471
+ readonly baselineCatalogModels: readonly RawEntry[];
472
+ readonly routedEntries: readonly RawEntry[];
473
+ readonly baseline: ReadonlyMap<string, number>;
474
+ readonly featured: readonly string[];
475
+ readonly modelPickerOrder?: readonly string[];
476
+ readonly accountSelectors?: readonly string[];
477
+ readonly wsEnabled: boolean;
478
+ readonly template: RawEntry | null;
479
+ readonly disabledModels: ReadonlySet<string>;
480
+ readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
481
+ readonly gatheredProviderNames: ReadonlySet<string>;
482
+ readonly pendingProviderNames?: ReadonlySet<string>;
483
+ readonly degradedProviderNames: ReadonlySet<string>;
484
+ readonly legacyCustomModelSlugs: ReadonlySet<string>;
485
+ readonly multiAgentMode: MultiAgentMode;
486
+ readonly multiAgentV2Enabled: boolean;
487
+ readonly keepNativeChatGptOnV1?: boolean;
488
+ readonly exactComboSlugs: ReadonlySet<string>;
489
+ readonly hasPhysicalComboProvider: boolean;
490
+ readonly includeNativeOpenAi: boolean;
491
+ readonly accountBoundEntries: readonly RawEntry[];
492
+ readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
493
+ readonly policy: ObservedCatalogMergePolicy;
494
+ readonly openaiContextCap?: NativeContextLimitsInput;
495
+ /** Exact display-only labels for bare native OpenAI models. */
496
+ readonly nativeDisplayNames?: Readonly<Record<string, string>>;
497
+ }
498
+
499
+ /**
500
+ * Deterministically merge one fully observed catalog state.
501
+ *
502
+ * Every non-catalog input is explicit so evidence-bound convergence cannot
503
+ * accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
504
+ */
505
+ export function mergeCatalogEntriesFromObservedState({
506
+ catalogModels,
507
+ baselineCatalogModels,
508
+ routedEntries,
509
+ baseline,
510
+ featured,
511
+ modelPickerOrder = [],
512
+ accountSelectors = [],
513
+ wsEnabled,
514
+ template,
515
+ disabledModels,
516
+ selectedModelsByProvider,
517
+ gatheredProviderNames,
518
+ pendingProviderNames = new Set(),
519
+ degradedProviderNames,
520
+ legacyCustomModelSlugs,
521
+ multiAgentMode,
522
+ multiAgentV2Enabled,
523
+ keepNativeChatGptOnV1,
524
+ exactComboSlugs,
525
+ hasPhysicalComboProvider,
526
+ includeNativeOpenAi,
527
+ accountBoundEntries,
528
+ suppressedBareNativeSlugs = new Set(),
529
+ policy,
530
+ openaiContextCap,
531
+ nativeDisplayNames,
532
+ }: ObservedCatalogMergeInput): RawEntry[] {
533
+ // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
534
+ // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
535
+ const detachedCatalogModels = catalogModels
536
+ .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
537
+ const detachedBaselineCatalogModels = baselineCatalogModels
538
+ .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
539
+ const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
540
+ // Track this invocation's generated custom rows, not ownership markers read from disk.
541
+ // Their builder already finalized exact native ladders and ordinary routed mock tiers.
542
+ const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
543
+ entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND));
544
+ const detachedAccountBoundEntries = accountBoundEntries
545
+ .map(entry => structuredClone(entry) as RawEntry);
546
+ const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
547
+ const legacyCustomModelKeys = new Set(
548
+ [...legacyCustomModelSlugs].map(slugEquivalenceKey),
549
+ );
550
+ const selectedModelKeysByProvider = new Map([...selectedModelsByProvider].map(([provider, models]) => (
551
+ [provider, new Set([...models].map(model => slugEquivalenceKey(routedSlug(provider, model))))] as const
552
+ )));
553
+ const freshAccountKeys = new Set(detachedAccountBoundEntries.flatMap(entry => (
554
+ typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
555
+ )));
556
+ const wouldSurviveUnreplaced = (entry: RawEntry): boolean => {
557
+ if (entry.owned_by === COMBO_NAMESPACE
558
+ || trustedAccountBoundNativeCatalogSlug(entry) !== undefined
559
+ || entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
560
+ || isOcxAuthoredRoutedEntry(entry)
561
+ || typeof entry.slug !== "string") return false;
562
+ const slug = entry.slug;
563
+ if (!slug.includes("/")) {
564
+ if (!includeNativeOpenAi || policy.nativeBackfillSlugs.includes(slug)) return false;
565
+ return policy.unsupportedNativeEntries === "preserve" || !isUnsupportedOpenAiNativeSlug(slug);
566
+ }
567
+ if (isRoutedModelCompatibilityExcluded(slug)) return false;
568
+ if (!hasPhysicalComboProvider && slug.startsWith(`${COMBO_NAMESPACE}/`)) return false;
569
+ const key = slugEquivalenceKey(slug);
570
+ if (freshAccountKeys.has(key)) return false;
571
+ if (disabledModelKeys.has(key)) return false;
572
+ const slash = slug.indexOf("/");
573
+ const provider = slug.slice(0, slash);
574
+ if (pendingProviderNames.has(provider)) return false;
575
+ const selected = selectedModelKeysByProvider.get(provider);
576
+ if (selected !== undefined && !selected.has(key)) return false;
577
+ return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
578
+ };
579
+ const validRoutedEntries = detachedRoutedEntries.filter(entry => {
580
+ return !isExactComboCatalogEntry(entry, exactComboSlugs)
581
+ || (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
582
+ });
583
+ const restorableCatalogKeys = new Set(detachedBaselineCatalogModels.flatMap(entry => (
584
+ wouldSurviveUnreplaced(entry) && typeof entry.slug === "string"
585
+ ? [slugEquivalenceKey(entry.slug)]
586
+ : []
587
+ )));
588
+ const unrestorableCatalogKeys = new Set(detachedCatalogModels.flatMap(entry => {
589
+ if (!wouldSurviveUnreplaced(entry) || typeof entry.slug !== "string") return [];
590
+ const key = slugEquivalenceKey(entry.slug);
591
+ return restorableCatalogKeys.has(key) ? [] : [key];
592
+ }));
593
+ const admittedRoutedEntries = validRoutedEntries.filter(entry => {
594
+ if (!isExactComboCatalogEntry(entry, exactComboSlugs)) return true;
595
+ const slug = entry.slug as string;
596
+ const key = slugEquivalenceKey(slug);
597
+ if (!unrestorableCatalogKeys.has(key)) return true;
598
+ if (policy.warningPolicy === "emit") warnComboUnrestorableShadowOnce(slug);
599
+ return false;
600
+ });
601
+ // A fresh non-custom row authoritatively resolves a historically ambiguous slug as a normal
602
+ // provider model. Persist that classification so the durable deletion evidence cannot remove
603
+ // the legitimate row during a later degraded refresh.
604
+ for (const entry of admittedRoutedEntries) {
605
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
606
+ if (!slug
607
+ || entry.opencodex_catalog_kind !== undefined
608
+ || entry.owned_by === COMBO_NAMESPACE
609
+ || !isOcxAuthoredRoutedEntry(entry)
610
+ || !legacyCustomModelKeys.has(slugEquivalenceKey(slug))) continue;
611
+ entry.opencodex_catalog_kind = CODEX_PROVIDER_MODEL_CATALOG_KIND;
612
+ }
613
+ const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
614
+ isExactComboCatalogEntry(entry, exactComboSlugs)
615
+ && typeof entry.description === "string"
616
+ && entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
617
+ )));
618
+ const rank = new Map(featured.map((slug, i) => [slug, i] as const));
619
+ const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
620
+ typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
621
+ )));
622
+ const freshEquivalent = (slug: string): boolean => (
623
+ freshEquivalentKeys.has(slugEquivalenceKey(slug))
624
+ );
625
+ const freshBareComboAliases = new Set(admittedRoutedEntries.flatMap(entry => (
626
+ typeof entry.slug === "string"
627
+ && !entry.slug.includes("/")
628
+ && entry.owned_by === COMBO_NAMESPACE
629
+ ? [entry.slug]
630
+ : []
631
+ )));
632
+ const staleComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
633
+ typeof entry.slug === "string"
634
+ && entry.owned_by === COMBO_NAMESPACE
635
+ && !freshEquivalent(entry.slug)
636
+ ? [slugEquivalenceKey(entry.slug)]
637
+ : []
638
+ )));
639
+ const currentNonComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
640
+ entry.owned_by !== COMBO_NAMESPACE && typeof entry.slug === "string"
641
+ ? [slugEquivalenceKey(entry.slug)]
642
+ : []
643
+ )));
644
+ const restoredComboShadows = detachedBaselineCatalogModels.filter(entry => {
645
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
646
+ if (!slug || entry.owned_by === COMBO_NAMESPACE) return false;
647
+ const key = slugEquivalenceKey(slug);
648
+ return staleComboKeys.has(key) && !currentNonComboKeys.has(key);
649
+ });
650
+ const catalogModelsForMerge = [...detachedCatalogModels, ...restoredComboShadows];
651
+ const nativePriority = (slug: string, fallback: unknown): number => {
652
+ const base = baseline.get(slug)
653
+ ?? (typeof fallback === "number" ? fallback : 9);
654
+ if (rank.has(slug)) return rank.get(slug)!;
655
+ return featured.length > 0 ? Math.max(base, featured.length + 100) : base;
656
+ };
657
+ const nativeSourceEntries = includeNativeOpenAi
658
+ ? catalogModelsForMerge
659
+ .filter(m => typeof m.slug === "string"
660
+ && !(m.slug as string).includes("/")
661
+ && m.owned_by !== COMBO_NAMESPACE
662
+ && (policy.unsupportedNativeEntries === "preserve"
663
+ || policy.nativeBackfillSlugs.includes(m.slug as string)
664
+ || !isUnsupportedOpenAiNativeSlug(m.slug as string)))
665
+ .map(m => {
666
+ const slug = m.slug as string;
667
+ // Fallback-quality entries (ocx synthesis / codex-rs model_info fallback: display_name
668
+ // stamped with the bare slug) are upgraded to the pinned upstream snapshot entry so a
669
+ // previously synthesized ladder (e.g. luna advertising ultra) self-heals on sync. A
670
+ // genuine catalog entry (real display name) is preserved untouched.
671
+ if (shouldUpgradeToUpstreamEntry(m)) {
672
+ const upstream = upstreamNativeEntry(slug)!;
673
+ const finished = finishUpstreamNativeEntry(upstream, 9, openaiContextCap);
674
+ finished.priority = nativePriority(slug, upstream.priority);
675
+ return finished;
676
+ }
677
+ const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) });
678
+ // Recompute spawn rank from current featured models, not a prior picker override.
679
+ delete preserved[SPAWN_PRIORITY_FIELD];
680
+ // Older natives kept from disk still need the mock top tiers (max + ultra always
681
+ // for subagent max spawns; wire-clamped to the model's real top rung).
682
+ if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
683
+ return preserved;
684
+ })
685
+ : [];
686
+ const native = nativeSourceEntries.filter(entry =>
687
+ typeof entry.slug !== "string"
688
+ || (!freshBareComboAliases.has(entry.slug) && !suppressedBareNativeSlugs.has(entry.slug))
689
+ );
690
+
691
+ // Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
692
+ // routed provider exposing the same id can never delete the native OpenAI/Codex base row.
693
+ // Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
694
+ const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
695
+ if (includeNativeOpenAi) {
696
+ for (const slug of policy.nativeBackfillSlugs) {
697
+ if (nativeSlugs.has(slug) || freshBareComboAliases.has(slug) || suppressedBareNativeSlugs.has(slug)) continue;
698
+ nativeSlugs.add(slug);
699
+ const entry = deriveEntry(
700
+ template ? JSON.parse(JSON.stringify(template)) : null,
701
+ slug,
702
+ "OpenAI native model (Codex OAuth passthrough).",
703
+ nativePriority(slug, upstreamNativeEntry(slug)?.priority),
704
+ undefined,
705
+ new Set(),
706
+ openaiContextCap,
707
+ );
708
+ entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority);
709
+ native.push(entry);
710
+ }
711
+ }
712
+
713
+ const nativeSourceBySlug = new Map([...nativeSourceEntries, ...native].flatMap(entry =>
714
+ typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
715
+ ));
716
+ const alignedAccountBoundEntries = detachedAccountBoundEntries.map(entry => {
717
+ // The explicit Reserve source is already chosen (actual row or documented Luna adaptation).
718
+ // A generic native merge must not replace its provenance or capability ladder.
719
+ if (isReserveCatalogProjection(entry)) return entry;
720
+ const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
721
+ const source = nativeSlug === undefined ? undefined : nativeSourceBySlug.get(nativeSlug);
722
+ if (!source) return entry;
723
+ const aligned = JSON.parse(JSON.stringify(source)) as RawEntry;
724
+ aligned.slug = entry.slug;
725
+ aligned.display_name = entry.display_name;
726
+ aligned.priority = entry.priority;
727
+ aligned.visibility = "list";
728
+ aligned.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
729
+ return aligned;
730
+ });
731
+
732
+ const freshSlugs = new Set(
733
+ admittedRoutedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
734
+ );
735
+ const existingRoutedEntries = catalogModelsForMerge.filter(m =>
736
+ typeof m.slug === "string"
737
+ && (m.slug.includes("/") || isNativeAliasCatalogEntry(m))
738
+ && trustedAccountBoundNativeCatalogSlug(m) === undefined
739
+ );
740
+ const preservedRoutedEntries = existingRoutedEntries.filter(entry => {
741
+ const slug = entry.slug as string;
742
+ if (freshEquivalent(slug)) return false;
743
+ if (isNativeAliasCatalogEntry(entry)) return exactComboSlugs.has(slug);
744
+ // Current custom rows are always regenerated from config, even while provider discovery is
745
+ // degraded. A marked row absent from the fresh projection is therefore an intentional delete.
746
+ if (entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND) return false;
747
+ // Before custom rows had a marker, a config deletion could otherwise be mistaken for a
748
+ // provider outage. Only explicit save-boundary evidence may classify an unmarked OpenCodex
749
+ // row; foreign and future-marked rows fail closed and remain preserved.
750
+ if (entry.opencodex_catalog_kind === undefined
751
+ && entry.owned_by !== COMBO_NAMESPACE
752
+ && isOcxAuthoredRoutedEntry(entry)
753
+ && legacyCustomModelKeys.has(slugEquivalenceKey(slug))) return false;
754
+ const provider = slug.slice(0, slug.indexOf("/"));
755
+ if (gatheredProviderNames.has(provider)) {
756
+ // A provider-local degraded observation preserves only that namespace. Authoritative empty
757
+ // catalogs and successful removals still delete stale rows even when another provider fails.
758
+ return degradedProviderNames.has(provider);
759
+ }
760
+ // Deleted/disabled providers cannot retain OpenCodex-authored ghosts. Foreign catalog rows
761
+ // remain outside provider ownership and survive unless a fresh row replaces their exact slug.
762
+ return !isOcxAuthoredRoutedEntry(entry);
763
+ });
764
+ // Retained rows bypass the builder. Recompute managed spawn ranks from current config
765
+ // before either display-order mode; a saved display override is not current roster authority.
766
+ const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
767
+ const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/"));
768
+ const rankOf = modelPickerRank(pickerOrder);
769
+ const featuredRankOf = modelPickerRank(featured);
770
+ const priorityStride = Math.max(accountSelectors.length, 1);
771
+ for (const entry of preservedRoutedEntries) {
772
+ const natural = entry[SPAWN_PRIORITY_FIELD];
773
+ if (typeof natural === "number") {
774
+ entry.priority = natural;
775
+ delete entry[SPAWN_PRIORITY_FIELD];
776
+ }
777
+ const slug = String(entry.slug);
778
+ if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
779
+ const featuredRank = featuredRankOf(slug);
780
+ entry.priority = featuredRank !== undefined
781
+ ? featuredRank * priorityStride
782
+ : (accountSelectors.length > 0 ? 1_000 : 0) + 5;
783
+ if (featuredRank !== undefined || fullPickerOrder) continue;
784
+ const pickerIndex = rankOf(slug);
785
+ if (pickerIndex !== undefined) {
786
+ entry[SPAWN_PRIORITY_FIELD] = entry.priority;
787
+ entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride;
788
+ }
789
+ }
790
+ let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
791
+ finalRoutedEntries = finalRoutedEntries.filter(entry => {
792
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
793
+ if (!slug.includes("/")) return true;
794
+ if (disabledModelKeys.has(slugEquivalenceKey(slug))) return false;
795
+ // Provider allowlists own provider rows, not a current combo's public alias. Exempt only an
796
+ // identity from this gather's generated combo projection: provider discovery may supply a
797
+ // spoofed `owned_by`, and persisted combo-shaped rows are not fresh authority.
798
+ if (freshExactComboEntries.has(entry)) return true;
799
+ const slash = slug.indexOf("/");
800
+ const provider = slug.slice(0, slash);
801
+ if (pendingProviderNames.has(provider)) return false;
802
+ const selected = selectedModelKeysByProvider.get(provider);
803
+ return selected === undefined || selected.has(slugEquivalenceKey(slug));
804
+ });
805
+ if (!hasPhysicalComboProvider) {
806
+ finalRoutedEntries = finalRoutedEntries.filter(entry => {
807
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
808
+ const comboOwned = slug.startsWith(`${COMBO_NAMESPACE}/`) || entry.owned_by === COMBO_NAMESPACE;
809
+ const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
810
+ return !comboOwned || freshSlugs.has(slug) || retainedNativeAlias;
811
+ });
812
+ }
813
+ finalRoutedEntries = finalRoutedEntries.filter(entry => {
814
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
815
+ const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
816
+ return retainedNativeAlias
817
+ || !isExactComboCatalogEntry(entry, exactComboSlugs)
818
+ || (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
819
+ });
820
+ // Reapply final catalog policy to rows preserved from disk. Those rows bypass
821
+ // gatherRoutedModels, so filtering only the freshly gathered list can resurrect an excluded id.
822
+ finalRoutedEntries = finalRoutedEntries.filter(entry =>
823
+ typeof entry.slug !== "string" || !isRoutedModelCompatibilityExcluded(entry.slug)
824
+ );
825
+ const accountBoundSlugs = new Set(alignedAccountBoundEntries.flatMap(entry =>
826
+ typeof entry.slug === "string" ? [entry.slug] : []
827
+ ));
828
+ finalRoutedEntries = finalRoutedEntries.filter(entry => {
829
+ if (typeof entry.slug !== "string" || !accountBoundSlugs.has(entry.slug)) return true;
830
+ if (freshSlugs.has(entry.slug) && policy.warningPolicy === "emit") {
831
+ warnAccountSelectorShadowedProviderOnce(entry.slug);
832
+ }
833
+ return false;
834
+ });
835
+ const finalRoutedEntrySet = new Set(finalRoutedEntries);
836
+ const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
837
+ if (!finalRoutedEntrySet.has(entry)) return false;
838
+ const slug = entry.slug as string;
839
+ const provider = slug.slice(0, slug.indexOf("/"));
840
+ return gatheredProviderNames.has(provider) && degradedProviderNames.has(provider);
841
+ }).length;
842
+ if (degradedPreservedCount > 0 && policy.warningPolicy === "emit") {
843
+ console.warn(`[opencodex] catalog sync: provider discovery degraded; preserving ${degradedPreservedCount} existing routed entr${degradedPreservedCount === 1 ? "y" : "ies"} on disk.`);
844
+ }
845
+
846
+ const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
847
+ const observedNativeSlugs = new Set(alignedAccountBoundEntries.flatMap(entry => {
848
+ const slug = trustedAccountBoundNativeCatalogSlug(entry);
849
+ return slug === undefined ? [] : [slug];
850
+ }));
851
+ for (const slug of policy.nativeBackfillSlugs) observedNativeSlugs.add(slug);
852
+ const mergedEntries = [...native, ...managedEntries].map(m => {
853
+ const reserveProjection = isReserveCatalogProjection(m);
854
+ const normalized = reserveProjection ? m : normalizeServiceTiers(m);
855
+ if (!reserveProjection && !isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
856
+ const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
857
+ const e = reserveProjection ? normalized : ensureStrictCatalogFields(normalized, {
858
+ preserveExactInputModalities: exactCombo,
859
+ isRouted: finalRoutedEntrySet.has(m),
860
+ });
861
+ // Mock-max universality (260709): preserved routed entries from disk may predate
862
+ // 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.
864
+ if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
865
+ const levels = Array.isArray(e.supported_reasoning_levels)
866
+ ? e.supported_reasoning_levels as Array<{ effort?: string }>
867
+ : [];
868
+ if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
869
+ levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
870
+ ?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
871
+ e.supported_reasoning_levels = levels;
872
+ }
873
+ }
874
+ if (wsEnabled) e.supports_websockets = true;
875
+ else {
876
+ delete e.supports_websockets;
877
+ // Match buildCatalogEntries: never advertise a websocket preference while WS is off.
878
+ delete e.prefer_websockets;
879
+ }
880
+ return e;
881
+ });
882
+ // Native enable/disable runs as the LAST pass so the upstream-upgrade branch above can never
883
+ // clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
884
+ // only their generated account row.
885
+ const versionedEntries = applyMultiAgentMode(
886
+ applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
887
+ multiAgentMode,
888
+ multiAgentV2Enabled,
889
+ { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
890
+ );
891
+ applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
892
+ for (const entry of versionedEntries) {
893
+ // Templates and account clones must not inherit the native row's overlay marker.
894
+ delete entry.opencodex_native_display_name;
895
+ const slug = recoverableNativeSlug(entry);
896
+ if (slug !== null) {
897
+ const label = nativeDisplayNames && Object.hasOwn(nativeDisplayNames, slug)
898
+ ? nativeDisplayNames[slug]?.trim() : undefined;
899
+ if (label && label !== entry.display_name) {
900
+ entry.opencodex_native_display_name = { slug, original: entry.display_name, applied: label };
901
+ entry.display_name = label;
902
+ }
903
+ }
904
+ const kind = entry.opencodex_catalog_kind;
905
+ if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
906
+ && kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND
907
+ && kind !== CODEX_PROVIDER_MODEL_CATALOG_KIND) continue;
908
+ // Canonicalize extension-field order after every normalizer. This keeps an unchanged catalog
909
+ // byte-idempotent whether an owned row was freshly built or retained from the prior pass.
910
+ delete entry.opencodex_catalog_kind;
911
+ entry.opencodex_catalog_kind = kind;
912
+ }
913
+ return versionedEntries;
914
+ }
915
+
916
+ /** Merge retained-sync rows using the process-observed Codex feature state. */
917
+ export function mergeCatalogEntriesForSync(
918
+ catalogModels: RawEntry[],
919
+ routedEntries: RawEntry[],
920
+ baseline: Map<string, number>,
921
+ featured: string[],
922
+ wsEnabled: boolean,
923
+ _goIds: Set<string> = new Set(),
924
+ template: RawEntry | null = null,
925
+ disabledModels: ReadonlySet<string> = new Set(),
926
+ gatheredProviderNames?: Set<string>,
927
+ multiAgentMode: MultiAgentMode = "default",
928
+ exactComboSlugs: ReadonlySet<string> = new Set(),
929
+ hasPhysicalComboProvider = false,
930
+ includeNativeOpenAi = true,
931
+ accountBoundEntries: readonly RawEntry[] = [],
932
+ legacyCustomModelSlugs: ReadonlySet<string> = new Set(),
933
+ suppressedBareNativeSlugs: ReadonlySet<string> = new Set(
934
+ routedEntries.flatMap(entry => (
935
+ isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
936
+ )),
937
+ ),
938
+ openaiContextCap?: NativeContextLimitsInput,
939
+ keepNativeChatGptOnV1 = false,
940
+ ): RawEntry[] {
941
+ // Retained for source compatibility with the original helper contract. Raw provider ids must
942
+ // not suppress same-named native rows; actual admitted combo entries own that decision now.
943
+ void _goIds;
944
+ const effectiveGatheredProviderNames = gatheredProviderNames ?? new Set(
945
+ routedEntries.flatMap(entry => {
946
+ // A slashed combo alias is not evidence that its public prefix is an authoritative provider
947
+ // namespace. Treating it as one would let the combo replace an unrestorable foreign row.
948
+ if (isExactComboCatalogEntry(entry, exactComboSlugs)) return [];
949
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
950
+ const slash = slug.indexOf("/");
951
+ return slash > 0 ? [slug.slice(0, slash)] : [];
952
+ }),
953
+ );
954
+ return mergeCatalogEntriesFromObservedState({
955
+ catalogModels,
956
+ baselineCatalogModels: [],
957
+ routedEntries,
958
+ baseline,
959
+ featured,
960
+ wsEnabled,
961
+ template,
962
+ disabledModels,
963
+ selectedModelsByProvider: new Map(),
964
+ gatheredProviderNames: effectiveGatheredProviderNames,
965
+ degradedProviderNames: new Set(),
966
+ legacyCustomModelSlugs,
967
+ multiAgentMode,
968
+ multiAgentV2Enabled: isMultiAgentV2Enabled(),
969
+ keepNativeChatGptOnV1,
970
+ exactComboSlugs,
971
+ hasPhysicalComboProvider,
972
+ includeNativeOpenAi,
973
+ accountBoundEntries,
974
+ suppressedBareNativeSlugs,
975
+ openaiContextCap,
976
+ policy: {
977
+ ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
978
+ warningPolicy: "emit",
979
+ },
980
+ });
981
+ }