@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -39,6 +39,10 @@ import {
39
39
  parsePersistedCodexRuntime,
40
40
  peekCodexRuntimeProcessCache,
41
41
  resolveAndPersistCodexRuntime,
42
+ resolveAndPersistCodexRuntimeAsync,
43
+ codexRuntimeSelectionIdentity,
44
+ codexRuntimeStateEpoch,
45
+ execCodexFileAsync,
42
46
  } from "../runtime";
43
47
  import type {
44
48
  DeepReadonly,
@@ -63,7 +67,9 @@ export type ReadonlyRawCatalog = DeepReadonly<RawCatalog>;
63
67
  interface BundledCatalogMemo {
64
68
  /** Selected runtime identity; must change when doctor/sync picks a different binary. */
65
69
  readonly key: string;
70
+ readonly inputs: string;
66
71
  readonly expiresAt: number;
72
+ readonly refreshAt: number;
67
73
  readonly epoch: number;
68
74
  readonly valueIdentity: string;
69
75
  readonly value: ReadonlyRawCatalog | null;
@@ -76,6 +82,9 @@ export interface BundledCatalogCacheState {
76
82
 
77
83
  let bundledCatalogEpoch = 0;
78
84
  let bundledCatalogCache: BundledCatalogMemo | null = null;
85
+ let bundledCatalogRetry: { inputs: string; epoch: number; after: number } | null = null;
86
+ let bundledCatalogFlight: { inputs: string; epoch: number; controller: AbortController; promise: Promise<ReadonlyRawCatalog | null> } | null = null;
87
+ const catalogAlwaysCurrent = () => true;
79
88
 
80
89
  function cloneAndDeepFreeze<T>(value: T): DeepReadonly<T> {
81
90
  const clone = (current: unknown): unknown => {
@@ -110,14 +119,18 @@ function publishBundledCatalogCache(
110
119
  const epoch = ++bundledCatalogEpoch;
111
120
  bundledCatalogCache = {
112
121
  key,
122
+ inputs: codexRuntimeSelectionIdentity({ discoverAlternatives: false }),
113
123
  expiresAt,
124
+ refreshAt: Math.min(expiresAt, Date.now() + 15_000),
114
125
  epoch,
115
126
  valueIdentity: `bundled:${epoch}`,
116
127
  value: value === null ? null : cloneAndDeepFreeze(value),
117
128
  };
129
+ bundledCatalogRetry = null;
118
130
  }
119
131
 
120
132
  function clearBundledCatalogCache(): void {
133
+ bundledCatalogFlight?.controller.abort();
121
134
  bundledCatalogEpoch += 1;
122
135
  bundledCatalogCache = null;
123
136
  }
@@ -162,12 +175,17 @@ export type ExecFile = (
162
175
  windowsHide: boolean;
163
176
  shell?: boolean;
164
177
  windowsVerbatimArguments?: boolean;
178
+ signal?: AbortSignal;
179
+ maxBuffer?: number;
165
180
  },
166
181
  ) => string;
167
182
 
168
183
  export interface BundledCatalogDeps {
184
+ /** Already-observed source for server clamping; null never triggers a probe. */
185
+ observedCatalog?: ReadonlyRawCatalog | null;
169
186
  commandCandidates?: () => string[];
170
187
  execFileSync?: ExecFile;
188
+ execFile?: (...args: Parameters<ExecFile>) => Promise<string>;
171
189
  onEffortClamp?: (diagnostic: EffortClampDiagnostic) => void;
172
190
  configDir?: string;
173
191
  env?: NodeJS.ProcessEnv;
@@ -224,11 +242,11 @@ export function codexShimCommandCandidates(): string[] {
224
242
  }
225
243
  }
226
244
 
227
- export function runCodexDebugModels(
245
+ export function runCodexDebugModels<T>(
228
246
  command: string,
229
- execFile: ExecFile,
247
+ execFile: (...args: Parameters<ExecFile>) => T,
230
248
  deps: Pick<BundledCatalogDeps, "env" | "platform" | "existsSync"> = {},
231
- ): string {
249
+ ): T {
232
250
  const args = ["debug", "models", "--bundled"];
233
251
  const invocation = codexExecInvocation(command, args, deps.platform ?? process.platform, {
234
252
  env: deps.env,
@@ -238,23 +256,19 @@ export function runCodexDebugModels(
238
256
  encoding: "utf8" as const,
239
257
  stdio: ["ignore", "pipe", "ignore"] as ["ignore", "pipe", "ignore"],
240
258
  timeout: 10_000,
259
+ maxBuffer: 64 * 1024 * 1024,
241
260
  windowsHide: true,
242
261
  ...invocation.options,
243
262
  });
244
263
  }
245
264
 
246
- export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): ReadonlyRawCatalog | null {
247
- const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.configDir && !deps.env;
248
- const execFile = deps.execFileSync ?? (execFileSync as unknown as ExecFile);
249
- // Prefer the single resolved runtime so sync/clamp never probe a different binary
250
- // than OpenCodex will launch. Tests may inject commandCandidates to stub probing.
251
- let cacheKey: string | null = null;
252
- const candidates = deps.commandCandidates?.() ?? (() => {
253
- const resolved = resolveAndPersistCodexRuntime({
265
+ function bundledRuntimeDeps(deps: BundledCatalogDeps) {
266
+ return {
254
267
  // Forward an INJECTED execFileSync only. Passing the real one unconditionally made
255
268
  // resolveCacheKey() bail out (it refuses to memoize injected-dep resolves), so every
256
269
  // catalog read re-ran the ~1s `codex --version` probe even on a warm cache hit.
257
270
  ...(deps.execFileSync ? { execFileSync: deps.execFileSync } : {}),
271
+ ...(deps.execFile ? { execFile: deps.execFile } : {}),
258
272
  configDir: deps.configDir,
259
273
  env: deps.env,
260
274
  platform: deps.platform,
@@ -266,28 +280,41 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
266
280
  // which alone can exceed the 3s budget `ocx claude` allows /api/claude-code. Priority
267
281
  // selection is identical either way; callers wanting discovery diagnostics opt in.
268
282
  discoverAlternatives: deps.discoverAlternatives ?? false,
269
- });
270
- if (useCache) {
271
- cacheKey = bundledRuntimeKey(resolved.runtime);
272
- }
273
- return [resolved.runtime.command];
274
- })();
283
+ };
284
+ }
285
+
286
+ function* bundledCatalogSteps(
287
+ candidates: string[],
288
+ cacheKey: string | null,
289
+ current: () => boolean = () => true,
290
+ publishFailure = true,
291
+ ): Generator<string, ReadonlyRawCatalog | null, string | null> {
292
+ if (cacheKey && bundledCatalogCache?.key === cacheKey && current()) {
293
+ // The driver validated this same command/version under the new pin/env inputs.
294
+ // Re-stamp the memo even if its TTL is due, so failed refresh keeps confirmed rows.
295
+ bundledCatalogCache = { ...bundledCatalogCache,
296
+ inputs: codexRuntimeSelectionIdentity({ discoverAlternatives: false }) };
297
+ }
275
298
  if (
276
- useCache
277
- && cacheKey
299
+ cacheKey
278
300
  && bundledCatalogCache
279
301
  && bundledCatalogCache.key === cacheKey
302
+ && bundledCatalogCache.inputs === codexRuntimeSelectionIdentity({ discoverAlternatives: false })
280
303
  && bundledCatalogCache.expiresAt > Date.now()
281
304
  ) {
305
+ if (!current()) return null;
306
+ bundledCatalogCache = { ...bundledCatalogCache, refreshAt: Date.now() + 15_000 };
282
307
  return bundledCatalogCache.value === null
283
308
  ? null
284
309
  : cloneAndDeepFreeze(bundledCatalogCache.value);
285
310
  }
286
311
  for (const command of unique(candidates)) {
287
- try {
288
- const catalog = parseCatalogJson(runCodexDebugModels(command, execFile, deps));
312
+ if (!current()) return null;
313
+ const output = yield command;
314
+ if (!current()) return null;
315
+ const catalog = output === null ? null : parseCatalogJson(output);
289
316
  if (catalog && findNativeTemplate(catalog)) {
290
- if (useCache && cacheKey) {
317
+ if (cacheKey) {
291
318
  publishBundledCatalogCache(
292
319
  cacheKey,
293
320
  Date.now() + BUNDLED_CATALOG_CACHE_MS,
@@ -297,9 +324,8 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
297
324
  }
298
325
  return cloneAndDeepFreeze(catalog);
299
326
  }
300
- } catch { /* try next candidate */ }
301
327
  }
302
- if (useCache && cacheKey) {
328
+ if (cacheKey && (publishFailure || bundledCatalogCache?.key !== cacheKey || !bundledCatalogCache?.value) && current()) {
303
329
  publishBundledCatalogCache(
304
330
  cacheKey,
305
331
  Date.now() + BUNDLED_CATALOG_CACHE_MS,
@@ -309,6 +335,109 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
309
335
  return null;
310
336
  }
311
337
 
338
+ /** Explicit synchronous CLI driver; request readers use the snapshot below. */
339
+ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): ReadonlyRawCatalog | null {
340
+ const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.execFile && !deps.configDir && !deps.env;
341
+ const resolved = deps.commandCandidates ? null : resolveAndPersistCodexRuntime(bundledRuntimeDeps(deps));
342
+ const inputs = codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps));
343
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
344
+ useCache ? bundledRuntimeKey(resolved!.runtime) : null,
345
+ () => inputs === codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps)));
346
+ let step = steps.next();
347
+ while (!step.done) {
348
+ let output: string | null = null;
349
+ try { output = runCodexDebugModels(step.value, deps.execFileSync ?? (execFileSync as unknown as ExecFile), deps); }
350
+ catch { /* try next candidate */ }
351
+ step = steps.next(output);
352
+ }
353
+ return step.value;
354
+ }
355
+
356
+ /** Same candidate/parser authority, with version and bundled-model execs off the event loop. */
357
+ export function loadBundledCodexCatalogAsync(
358
+ deps: BundledCatalogDeps = {},
359
+ isCurrent: () => boolean = catalogAlwaysCurrent,
360
+ signal?: AbortSignal,
361
+ ): Promise<ReadonlyRawCatalog | null> {
362
+ if (signal?.aborted || !isCurrent()) return Promise.resolve(null);
363
+ const view = (promise: Promise<ReadonlyRawCatalog | null>) => {
364
+ if (!signal) return promise.then(value => isCurrent() ? value : null);
365
+ let abort: (() => void) | undefined;
366
+ return Promise.race([promise, new Promise<null>(resolve => {
367
+ abort = () => resolve(null);
368
+ signal.addEventListener("abort", abort, { once: true });
369
+ if (signal.aborted) abort();
370
+ })]).then(value => isCurrent() && !signal.aborted ? value : null)
371
+ .finally(() => { if (abort) signal.removeEventListener("abort", abort); });
372
+ };
373
+ const runtimeDeps = bundledRuntimeDeps(deps);
374
+ const inputs = codexRuntimeSelectionIdentity(runtimeDeps);
375
+ const epoch = bundledCatalogEpoch;
376
+ const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.execFile && !deps.configDir && !deps.env;
377
+ if (useCache && bundledCatalogFlight?.inputs === inputs && bundledCatalogFlight.epoch === epoch) {
378
+ return view(bundledCatalogFlight.promise);
379
+ }
380
+ const controller = new AbortController();
381
+ const flight = { inputs, epoch, controller, promise: Promise.resolve<ReadonlyRawCatalog | null>(null) };
382
+ if (useCache && !signal && isCurrent === catalogAlwaysCurrent) {
383
+ bundledCatalogFlight?.controller.abort();
384
+ bundledCatalogFlight = flight;
385
+ }
386
+ const abort = () => controller.abort();
387
+ signal?.addEventListener("abort", abort, { once: true });
388
+ if (signal?.aborted) abort();
389
+ let timer: ReturnType<typeof setTimeout> | undefined;
390
+ let deadlineReached = false;
391
+ const work = (async () => {
392
+ const active = () => !controller.signal.aborted && isCurrent() && bundledCatalogEpoch === epoch;
393
+ const resolved = deps.commandCandidates ? null : await resolveAndPersistCodexRuntimeAsync({ ...runtimeDeps, signal: controller.signal },
394
+ () => active() && codexRuntimeSelectionIdentity(runtimeDeps) === inputs);
395
+ if (!active() || (!deps.commandCandidates && !resolved)) return null;
396
+ flight.inputs = codexRuntimeSelectionIdentity(runtimeDeps);
397
+ const runtimeEpoch = codexRuntimeStateEpoch();
398
+ const current = () => active() && runtimeEpoch === codexRuntimeStateEpoch()
399
+ && flight.inputs === codexRuntimeSelectionIdentity(runtimeDeps);
400
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
401
+ useCache ? bundledRuntimeKey(resolved!.runtime) : null, current, false);
402
+ let step = steps.next();
403
+ while (!step.done) {
404
+ let output: string | null = null;
405
+ try { output = await runCodexDebugModels(step.value,
406
+ (file, args, options) => (deps.execFile ?? execCodexFileAsync)(file, args, { ...options, signal: controller.signal }), deps); }
407
+ catch { /* preserve last confirmed evidence on failure */ }
408
+ step = steps.next(output);
409
+ }
410
+ return step.value;
411
+ })().catch(() => null);
412
+ flight.promise = Promise.race([work, new Promise<null>(resolve => {
413
+ timer = setTimeout(() => { deadlineReached = true; controller.abort(); resolve(null); }, 45_000);
414
+ timer.unref?.();
415
+ })]).then(value => {
416
+ if (useCache && value === null && (!controller.signal.aborted || deadlineReached)
417
+ && isCurrent() && bundledCatalogEpoch === epoch
418
+ && flight.inputs === codexRuntimeSelectionIdentity(runtimeDeps)) {
419
+ bundledCatalogRetry = { inputs: flight.inputs, epoch, after: Date.now() + BUNDLED_CATALOG_CACHE_MS };
420
+ }
421
+ return value;
422
+ }).finally(() => {
423
+ clearTimeout(timer);
424
+ signal?.removeEventListener("abort", abort);
425
+ controller.abort();
426
+ if (bundledCatalogFlight === flight) bundledCatalogFlight = null;
427
+ });
428
+ return view(flight.promise);
429
+ }
430
+
431
+ /** Serve only the same selection's last confirmed catalog; refresh never holds a request. */
432
+ export function bundledCodexCatalogSnapshot(): ReadonlyRawCatalog | null {
433
+ const inputs = codexRuntimeSelectionIdentity({ discoverAlternatives: false });
434
+ const memo = bundledCatalogCache?.inputs === inputs ? bundledCatalogCache : null;
435
+ const cooling = bundledCatalogRetry?.inputs === inputs && bundledCatalogRetry.epoch === bundledCatalogEpoch
436
+ && bundledCatalogRetry.after > Date.now();
437
+ if ((!memo || memo.refreshAt <= Date.now()) && !cooling) void loadBundledCodexCatalogAsync().catch(() => { /* bounded retry */ });
438
+ return memo?.value ? cloneAndDeepFreeze(memo.value) : null;
439
+ }
440
+
312
441
  export type CatalogGatherProcessLocalObservation =
313
442
  | Readonly<{ state: "unused" }>
314
443
  | Readonly<{ state: "used"; epoch: number; valueIdentity: string }>;
@@ -524,7 +653,7 @@ export function loadCatalogForSync(path: string): RawCatalog | null {
524
653
 
525
654
  export function readCurrentCatalogOrCache(): RawCatalog | null {
526
655
  const path = readCodexCatalogPath();
527
- const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
656
+ const bundled = isDefaultCatalogPath(path) ? bundledCodexCatalogSnapshot() : null;
528
657
  if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
529
658
  return readCatalog(path) ?? readCatalog(activeCodexModelsCachePath());
530
659
  }
@@ -547,7 +676,7 @@ export function readCurrentCodexModelsCache(): RawCatalog | null {
547
676
 
548
677
  export function loadCatalogTemplate(): RawEntry | null {
549
678
  const catalogPath = readCodexCatalogPath();
550
- const bundled = loadBundledCodexCatalog();
679
+ const bundled = bundledCodexCatalogSnapshot();
551
680
  // Template inheritance only. The validity gates in this file keep `findNativeTemplate`
552
681
  // so a catalog carrying only a newly launched native row stays valid (#2813).
553
682
  const native = findSupportedNativeTemplate(readCatalog(catalogPath))
@@ -218,6 +218,7 @@ export function resolveComboCatalogMember(
218
218
  contextCap?: number,
219
219
  callerFallback?: ComboCatalogMemberFallback,
220
220
  metadataModelIdCaseFold?: boolean,
221
+ metadataConfigDir?: string,
221
222
  ): CatalogModel | undefined {
222
223
  const existing = memberByKey.get(targetKey(target));
223
224
  const prov = providers.get(target.provider);
@@ -275,7 +276,7 @@ export function resolveComboCatalogMember(
275
276
  // supplies a vision sidecar. Apply the same provider hints used for thin
276
277
  // rows before deriving a combo from this complete row.
277
278
  const hinted = prov && isModelVisionSidecarConsumer(prov, existing.id)
278
- ? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold)
279
+ ? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir)
279
280
  : existing;
280
281
  const capped = applyProviderContextCap(hinted.contextWindow, contextCap);
281
282
  if (capped === undefined || capped === existing.contextWindow) {
@@ -298,7 +299,7 @@ export function resolveComboCatalogMember(
298
299
  provider: target.provider,
299
300
  };
300
301
  const hinted = prov
301
- ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold)
302
+ ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir)
302
303
  : base;
303
304
  const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
304
305
  ? hinted.contextWindow
@@ -47,6 +47,7 @@ import {
47
47
  displayCodexRuntimePath,
48
48
  persistEffortClamp,
49
49
  resolveAndPersistCodexRuntime,
50
+ getCodexRuntimeSnapshot,
50
51
  UNCLAMPABLE_REASONING_EFFORTS,
51
52
  type EffortClampDiagnostic,
52
53
  } from "../runtime";
@@ -86,8 +87,14 @@ export function shouldApplyNativeEffortClamp(
86
87
  && isCanonicalOpenAiForwardProvider(provider);
87
88
  }
88
89
 
89
- export function catalogModelEfforts(slugs: readonly string[]): Map<string, string[]> {
90
- const out = new Map<string, string[]>();
90
+ export interface CatalogModelLadder {
91
+ readonly efforts: string[];
92
+ readonly defaultEffort?: string;
93
+ }
94
+
95
+ /** Each slug's reasoning ladder and default as the written Codex catalog offers them. */
96
+ export function catalogModelLadders(slugs: readonly string[]): Map<string, CatalogModelLadder> {
97
+ const out = new Map<string, CatalogModelLadder>();
91
98
  if (slugs.length === 0) return out;
92
99
  const catalog = readCatalog(readCodexCatalogPath());
93
100
  if (!catalog) return out;
@@ -101,11 +108,19 @@ export function catalogModelEfforts(slugs: readonly string[]): Map<string, strin
101
108
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
102
109
  : [];
103
110
  const efforts = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
104
- if (efforts.length > 0) out.set(callerSlug, efforts);
111
+ if (efforts.length === 0) continue;
112
+ const defaultEffort = typeof entry.default_reasoning_level === "string" && efforts.includes(entry.default_reasoning_level)
113
+ ? entry.default_reasoning_level
114
+ : undefined;
115
+ out.set(callerSlug, defaultEffort === undefined ? { efforts } : { efforts, defaultEffort });
105
116
  }
106
117
  return out;
107
118
  }
108
119
 
120
+ export function catalogModelEfforts(slugs: readonly string[]): Map<string, string[]> {
121
+ return new Map([...catalogModelLadders(slugs)].map(([slug, ladder]) => [slug, ladder.efforts]));
122
+ }
123
+
109
124
  export function catalogEntryEfforts(entry: RawEntry): string[] {
110
125
  const levels = Array.isArray(entry.supported_reasoning_levels)
111
126
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
@@ -366,7 +381,9 @@ export function supportedCodexReasoningEffortsFromObservedCatalog(
366
381
  }
367
382
 
368
383
  export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): ReadonlySet<string> | null {
369
- return supportedCodexReasoningEffortsFromObservedCatalog(loadBundledCodexCatalog(deps));
384
+ return supportedCodexReasoningEffortsFromObservedCatalog(
385
+ deps.observedCatalog !== undefined ? deps.observedCatalog : loadBundledCodexCatalog(deps),
386
+ );
370
387
  }
371
388
 
372
389
  export function clampedDefaultEffort(original: string, surviving: readonly string[]): string {
@@ -558,7 +575,11 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl
558
575
 
559
576
  let runtimePath = "codex";
560
577
  let runtimeVersion: string | null = null;
561
- if (!deps.commandCandidates) {
578
+ if (deps.observedCatalog !== undefined && !deps.commandCandidates) {
579
+ const snapshot = getCodexRuntimeSnapshot();
580
+ runtimePath = snapshot.runtime.command;
581
+ runtimeVersion = snapshot.runtime.version;
582
+ } else if (!deps.commandCandidates) {
562
583
  try {
563
584
  const resolved = resolveAndPersistCodexRuntime({
564
585
  execFileSync: deps.execFileSync,
@@ -148,6 +148,8 @@ export interface CapturedModelsRequest {
148
148
  }
149
149
 
150
150
  export interface CapturedProviderGather {
151
+ /** Public metadata reads and refreshes stay bound to this admission root. */
152
+ readonly metadataConfigDir: string;
151
153
  readonly name: string;
152
154
  readonly provider: OcxProviderConfig;
153
155
  readonly discovery: ResolvedProviderModelDiscovery;
@@ -354,6 +356,7 @@ export function captureProviderGather(
354
356
  retainConfiguredModelIds?: ReadonlySet<string>,
355
357
  config?: Pick<OcxConfig, "providers">,
356
358
  ): CapturedProviderGather {
359
+ const metadataConfigDir = getConfigDir();
357
360
  const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
358
361
  enrichProviderFromRegistry(name, enriched);
359
362
  const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
@@ -428,6 +431,7 @@ export function captureProviderGather(
428
431
  const effectiveAlias = effectiveProviderAliasDecision(name, configured, config);
429
432
  return Object.freeze({
430
433
  name,
434
+ metadataConfigDir,
431
435
  provider,
432
436
  discovery,
433
437
  policy,
@@ -482,6 +486,7 @@ export function captureGatherFlight(
482
486
  providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1",
483
487
  providers.map(provider => ({
484
488
  name: provider.name,
489
+ metadataConfigDir: provider.metadataConfigDir,
485
490
  // `fetch` is a caller-owned transport executor, not admitted state: the
486
491
  // outbound transport honors it so a caller can supply its own HTTP path.
487
492
  // It is the one member of a provider row that is legitimately a function,
@@ -35,7 +35,7 @@ import { modelInList } from "../../types";
35
35
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
36
36
  import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
37
37
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
38
- import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
38
+ import { azureVendorModelMetadata, enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
39
39
  import {
40
40
  captureFastPolicyAuthority,
41
41
  fastPolicyForModel,
@@ -217,9 +217,11 @@ export function routedMaxOutputTokens(
217
217
  model: CatalogModel,
218
218
  metadataId = model.id,
219
219
  metadataModelIdCaseFold?: boolean,
220
+ metadataConfigDir?: string,
220
221
  ): number | undefined {
221
222
  const discovered = positiveSafeInteger(model.maxOutputTokens);
222
- const generated = generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold);
223
+ const generated = positiveSafeInteger(azureVendorModelMetadata(provider.baseUrl, metadataId, metadataConfigDir)?.maxTokens)
224
+ ?? generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold);
223
225
  const configured = positiveSafeInteger(
224
226
  modelRecordValue(provider.modelMaxOutputTokens, model.id),
225
227
  );
@@ -255,6 +257,7 @@ export function applyProviderConfigHints(
255
257
  providerCap?: number,
256
258
  metadataModelIdCaseFold?: boolean,
257
259
  effectiveAlias?: string | null,
260
+ metadataConfigDir?: string,
258
261
  ): CatalogModel {
259
262
  const staticPolicy = resolveModelPolicy({
260
263
  providerName: name,
@@ -276,13 +279,14 @@ export function applyProviderConfigHints(
276
279
  : model.providerAlias;
277
280
  const configuredCap = staticPolicy.model.contextWindow ?? configuredContextWindow(prov, model.id);
278
281
  const configuredMaxInput = staticPolicy.model.maxInputTokens;
279
- const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
282
+ const azureMetadata = azureVendorModelMetadata(prov.baseUrl, model.id, metadataConfigDir);
283
+ const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold, metadataConfigDir);
280
284
  const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
281
285
  // The resolver owns exact capability precedence and legacy exact/colon-family/case-fold fallback.
282
286
  // Removing an exact capability row therefore returns this projection to legacy-map inference.
283
- let inputModalities = staticPolicy.model.inputModalities
284
- ? [...staticPolicy.model.inputModalities]
285
- : undefined;
287
+ const resolvedModalities = staticPolicy.model.inputModalities ?? model.inputModalities
288
+ ?? (azureMetadata?.input ? modelInputModalities({ id: model.id, input_modalities: azureMetadata.input }, undefined) : undefined);
289
+ let inputModalities = resolvedModalities ? [...resolvedModalities] : undefined;
286
290
  // The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time
287
291
  // planning aligned. The catalog must still advertise image input — the Codex app
288
292
  // gates attachments client-side on input_modalities, and a text-only entry would block images
@@ -319,6 +323,7 @@ export function applyProviderConfigHints(
319
323
  ...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
320
324
  });
321
325
  const hintedWindow = projectedLimits.contextWindow
326
+ ?? positiveSafeInteger(azureMetadata?.contextWindow)
322
327
  ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
323
328
  const hinted = {
324
329
  ...modelWithoutServiceTier,
@@ -383,8 +388,9 @@ export function catalogHintsFromProviderConfig(
383
388
  contextCap?: number,
384
389
  metadataModelIdCaseFold?: boolean,
385
390
  effectiveAlias?: string | null,
391
+ metadataConfigDir?: string,
386
392
  ): Partial<CatalogModel> {
387
- const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias);
393
+ const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias, metadataConfigDir);
388
394
  const { provider: _provider, id: _id, ...hints } = hinted;
389
395
  return hints;
390
396
  }
@@ -396,8 +402,9 @@ export function applyConfigHintsToCachedModels(
396
402
  contextCap?: number,
397
403
  metadataModelIdCaseFold?: boolean,
398
404
  effectiveAlias?: string | null,
405
+ metadataConfigDir?: string,
399
406
  ): CatalogModel[] {
400
- return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
407
+ return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias, metadataConfigDir));
401
408
  }
402
409
 
403
410
  /** Catalog slugs whose configured model must not gain a missing synthetic max rung. */
@@ -230,6 +230,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
230
230
  seedVertexDefault?: boolean;
231
231
  retainComboTargets?: boolean;
232
232
  metadataModelIdCaseFold?: boolean;
233
+ metadataConfigDir?: string;
233
234
  }): { models: CatalogModel[]; droppedConfiguredIds: string[] } {
234
235
  const {
235
236
  name,
@@ -240,6 +241,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
240
241
  seedVertexDefault,
241
242
  retainComboTargets = true,
242
243
  metadataModelIdCaseFold,
244
+ metadataConfigDir,
243
245
  } = opts;
244
246
  const out = [...opts.models];
245
247
  const present = new Set(out.map(model => model.id));
@@ -248,7 +250,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
248
250
  if (present.has(candidate.id)) continue;
249
251
  const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
250
252
  if (dated) {
251
- out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold));
253
+ out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir));
252
254
  present.add(candidate.id);
253
255
  continue;
254
256
  }
@@ -93,7 +93,7 @@ export const CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL = NATIVE_GPT61_SOL_MODEL;
93
93
  *
94
94
  * `gpt-daybreak-blue-latest` stays gated. It has no shipped catalog row anywhere, so absence is
95
95
  * the only signal that exists for it, and the ungating decision was scoped to the flagships.
96
- * Evidence: devlog/_plan/260904_flagship_native_always_visible/.
96
+ * Evidence: devlog/_fin/260904_flagship_native_always_visible/.
97
97
  */
98
98
  export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
99
99
  NATIVE_DAYBREAK_BLUE_MODEL,
@@ -0,0 +1,38 @@
1
+ import type { RawEntry } from "./parsing";
2
+
3
+ /** Match the opt-in tier spelling without coercing malformed catalog values to strings. */
4
+ function isUltraFast(value: unknown): boolean {
5
+ return typeof value === "string" && value.trim().toLowerCase() === "ultrafast";
6
+ }
7
+
8
+ /** Recognize an explicitly supplied Ultra Fast descriptor by its string-valued id. */
9
+ function isUltraFastDescriptor(value: unknown): boolean {
10
+ return value !== null && typeof value === "object" && "id" in value && isUltraFast(value.id);
11
+ }
12
+
13
+ /** Restore only exact-row operator metadata, after fresh provider capabilities are applied. */
14
+ export function preserveOperatorUltraFastTiers(entry: RawEntry, previous: RawEntry | undefined, enabled: boolean): void {
15
+ // A native template or stale row is not authority for this opt-in. Keep all other fresh
16
+ // tier policy, including Fast, while replacing only Ultra Fast from the exact persisted row.
17
+ for (const [field, matches] of [
18
+ ["service_tiers", isUltraFastDescriptor],
19
+ ["additional_speed_tiers", isUltraFast],
20
+ ] as const) {
21
+ const current = entry[field];
22
+ const fresh = Array.isArray(current) ? current.filter(value => !matches(value)) : [];
23
+ const old = previous?.[field];
24
+ const supplied = enabled && Array.isArray(old) ? old.find(matches) : undefined;
25
+ if (supplied !== undefined) {
26
+ entry[field] = [...fresh, structuredClone(supplied)];
27
+ } else if (Array.isArray(current) && fresh.length !== current.length) {
28
+ if (fresh.length > 0) entry[field] = fresh;
29
+ else delete entry[field];
30
+ }
31
+ }
32
+ const hasDeclaration = (Array.isArray(entry.service_tiers) && entry.service_tiers.some(isUltraFastDescriptor))
33
+ || (Array.isArray(entry.additional_speed_tiers) && entry.additional_speed_tiers.some(isUltraFast));
34
+ for (const field of ["service_tier", "default_service_tier"] as const) {
35
+ if (enabled && hasDeclaration && previous && isUltraFast(previous[field])) entry[field] = previous[field];
36
+ else if (isUltraFast(entry[field])) delete entry[field];
37
+ }
38
+ }