@bitkyc08/opencodex 2.11.1 → 2.12.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 (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -58,6 +58,7 @@ import {
58
58
  parseAccountPriority,
59
59
  } from "./pool-rotation";
60
60
  import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
61
+ import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "./plan";
61
62
  export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
62
63
  export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
63
64
  import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
@@ -103,7 +104,8 @@ import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types"
103
104
  import type { CatalogDisposition } from "./convergence-types";
104
105
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
105
106
  import { providerCodexAccountMode } from "../providers/registry";
106
- import { readBoundedResponseBody } from "../lib/bounded-body";
107
+ import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../lib/bounded-body";
108
+ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
107
109
  import {
108
110
  oauthAccountHealthFields,
109
111
  projectCodexAccountHealth,
@@ -208,16 +210,11 @@ function codexAccountPersistenceConflict(
208
210
  : undefined;
209
211
  }
210
212
 
211
- function isThirtyDayOnlyPlan(plan: string | null | undefined): boolean {
212
- const normalized = plan?.trim().toLowerCase();
213
- return normalized === "go" || normalized === "free";
214
- }
215
-
216
213
  function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
217
214
  quota: T,
218
- plan: string | null | undefined,
215
+ plan: unknown,
219
216
  ): T {
220
- if (!quota || !isThirtyDayOnlyPlan(plan)) return quota;
217
+ if (!quota || !isThirtyDayOnlyCodexPlan(plan)) return quota;
221
218
  return {
222
219
  ...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
223
220
  ...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
@@ -233,14 +230,15 @@ function poolAccountDto(
233
230
  paused: boolean,
234
231
  priority: number,
235
232
  ): CodexAuthAccountDto {
236
- const quota = quotaForPlan(quotaResult.quota, account.plan);
233
+ const plan = codexPlanValue(account.plan);
234
+ const quota = quotaForPlan(quotaResult.quota, plan);
237
235
  const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
238
236
  const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
239
237
  return {
240
238
  id: account.id,
241
239
  email: maskEmail(account.email) ?? account.email,
242
240
  ...(account.alias !== undefined ? { alias: account.alias } : {}),
243
- ...(account.plan !== undefined ? { plan: account.plan } : {}),
241
+ ...(plan !== undefined ? { plan } : {}),
244
242
  ...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}),
245
243
  isMain: false,
246
244
  paused,
@@ -340,6 +338,43 @@ function safeResetCreditConsumeDto(input: unknown): { code: string } {
340
338
  return { code: typeof obj.code === "string" ? obj.code : "unknown" };
341
339
  }
342
340
 
341
+ type ResetCreditJsonRead =
342
+ | { ok: true; value: unknown }
343
+ | { ok: false };
344
+
345
+ function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
346
+ if (!body) return;
347
+ try {
348
+ void body.cancel().catch(() => undefined);
349
+ } catch {
350
+ // Some stream implementations throw synchronously from cancel().
351
+ }
352
+ }
353
+
354
+ async function readResetCreditJson(
355
+ response: Response,
356
+ signal: AbortSignal,
357
+ ): Promise<ResetCreditJsonRead> {
358
+ const declaredLength = Number(response.headers.get("content-length"));
359
+ if (Number.isSafeInteger(declaredLength)
360
+ && declaredLength >= 0
361
+ && declaredLength > BOUNDED_BODY_MAX_BYTES) {
362
+ cancelResponseBodyWithoutWaiting(response.body);
363
+ return { ok: false };
364
+ }
365
+ try {
366
+ const body = await readBoundedResponseBody(response, {
367
+ signal,
368
+ maxBytes: BOUNDED_BODY_MAX_BYTES,
369
+ fatalUtf8: true,
370
+ });
371
+ if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
372
+ return { ok: true, value: JSON.parse(body.text) as unknown };
373
+ } catch {
374
+ return { ok: false };
375
+ }
376
+ }
377
+
343
378
  export function isUnverifiedCodexImportEnabled(): boolean {
344
379
  return process.env[MANUAL_IMPORT_ENV] === "1";
345
380
  }
@@ -398,7 +433,7 @@ const POOL_CACHE_TTL = 5 * 60_000;
398
433
  const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
399
434
 
400
435
  function nonEmptyPlan(value: unknown): string | null {
401
- return typeof value === "string" && value.trim() !== "" ? value : null;
436
+ return codexPlanValue(value) ?? null;
402
437
  }
403
438
 
404
439
  function isRuntimeConfig(config: OcxConfig): boolean {
@@ -1334,7 +1369,7 @@ export async function handleCodexAuthAPI(
1334
1369
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
1335
1370
  if (!isUnverifiedCodexImportEnabled()) return manualImportDisabledResponse();
1336
1371
 
1337
- let body: { id: string; email: string; plan?: string; accessToken: string; refreshToken: string; chatgptAccountId: string };
1372
+ let body: { id: string; email: string; plan?: unknown; accessToken: string; refreshToken: string; chatgptAccountId: string };
1338
1373
  try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
1339
1374
  if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
1340
1375
  return jsonResponse({ error: "Missing required fields" }, 400);
@@ -1349,8 +1384,9 @@ export async function handleCodexAuthAPI(
1349
1384
  const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
1350
1385
  if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
1351
1386
  // 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
1387
+ const plan = codexPlanValue(body.plan);
1352
1388
  const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
1353
- const collision = checkAccountIdCollision(derivedAccountId, body.email, body.plan);
1389
+ const collision = checkAccountIdCollision(derivedAccountId, body.email, plan);
1354
1390
  if (collision.collision) {
1355
1391
  return jsonResponse({ error: collision.reason }, 400);
1356
1392
  }
@@ -1363,7 +1399,12 @@ export async function handleCodexAuthAPI(
1363
1399
  const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
1364
1400
  if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
1365
1401
  const addedAccount = withCodexAccountLogLabel(
1366
- { id: body.id, email: body.email, plan: body.plan, isMain: false },
1402
+ {
1403
+ id: body.id,
1404
+ email: body.email,
1405
+ ...(plan !== undefined ? { plan } : {}),
1406
+ isMain: false,
1407
+ },
1367
1408
  latestConfig.codexAccounts ?? [],
1368
1409
  );
1369
1410
  const persistence = persistNewCodexAccount(
@@ -1679,21 +1720,44 @@ export async function handleCodexAuthAPI(
1679
1720
 
1680
1721
  try {
1681
1722
  const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
1682
- const resp = await fetch(
1683
- "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
1684
- {
1685
- headers: {
1686
- Authorization: `Bearer ${auth.accessToken}`,
1687
- "ChatGPT-Account-Id": auth.chatgptAccountId,
1688
- },
1689
- signal: AbortSignal.timeout(8000),
1690
- },
1691
- );
1692
- if (!resp.ok) {
1693
- await resp.body?.cancel().catch(() => {});
1694
- return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1723
+ const linkedSignal = signalWithTimeout(8000, req.signal);
1724
+ let detachBodyAbort = () => {};
1725
+ try {
1726
+ let resp: Response;
1727
+ try {
1728
+ resp = await fetch(
1729
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
1730
+ {
1731
+ headers: {
1732
+ Authorization: `Bearer ${auth.accessToken}`,
1733
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
1734
+ },
1735
+ signal: linkedSignal.signal,
1736
+ },
1737
+ );
1738
+ } catch (error) {
1739
+ if (linkedSignal.signal.aborted) {
1740
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
1741
+ }
1742
+ throw error;
1743
+ }
1744
+ // Own the response body before the bounded reader attaches. If the client
1745
+ // disconnects in that narrow window, Bun otherwise tears down the native
1746
+ // body off the awaited path and can report an unhandled rejection.
1747
+ detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
1748
+ if (!resp.ok) {
1749
+ await resp.body?.cancel().catch(() => {});
1750
+ return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1751
+ }
1752
+ const parsed = await readResetCreditJson(resp, linkedSignal.signal);
1753
+ if (!parsed.ok) {
1754
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
1755
+ }
1756
+ return jsonResponse(safeResetCreditsDto(parsed.value));
1757
+ } finally {
1758
+ detachBodyAbort();
1759
+ linkedSignal.cleanup();
1695
1760
  }
1696
- return jsonResponse(safeResetCreditsDto(await resp.json()));
1697
1761
  });
1698
1762
  return result.ok ? result.value : result.response;
1699
1763
  } catch (e) {
@@ -5,6 +5,7 @@ import { loadConfig } from "../config";
5
5
  import { resolveCodexHomeDir } from "./home";
6
6
  import { extractAccountId } from "../oauth/chatgpt";
7
7
  import { isSelectableCodexPoolAccount } from "./account-id";
8
+ import { codexPlanKey } from "./plan";
8
9
 
9
10
  export interface CodexTokens {
10
11
  access_token: string;
@@ -78,8 +79,9 @@ function normalizedEmail(email: string | undefined | null): string | null {
78
79
  return trimmed || null;
79
80
  }
80
81
 
81
- function isWorkspacePlan(plan: string | undefined | null): boolean {
82
- return !!plan && /team|business|enterprise|workspace|edu/i.test(plan);
82
+ function isWorkspacePlan(plan: unknown): boolean {
83
+ const key = codexPlanKey(plan);
84
+ return !!key && /team|business|enterprise|workspace|edu/.test(key);
83
85
  }
84
86
 
85
87
  // Main login and managed pool accounts are separate duplicate buckets.
@@ -88,7 +90,7 @@ function isWorkspacePlan(plan: string | undefined | null): boolean {
88
90
  export function checkAccountIdCollision(
89
91
  chatgptAccountId: string,
90
92
  email?: string | null,
91
- plan?: string | null,
93
+ plan?: unknown,
92
94
  excludeAccountId?: string | null,
93
95
  ): { collision: true; reason: string } | { collision: false } {
94
96
  const candidateEmail = normalizedEmail(email);
@@ -253,11 +253,11 @@ export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
253
253
  }
254
254
 
255
255
  export function nativeOpenAiSlugs(): string[] {
256
- const live = listCatalogNativeSlugs();
256
+ const live = catalogNativeSlugs();
257
257
  return live.length > 0 ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]) : NATIVE_OPENAI_MODELS;
258
258
  }
259
259
 
260
- export function listCatalogNativeSlugs(): string[] {
260
+ function catalogNativeSlugs(): string[] {
261
261
  const cat = readCurrentCatalogOrCache();
262
262
  const models = cat?.models ?? [];
263
263
  const live = models.flatMap(entry => {
@@ -271,7 +271,11 @@ export function listCatalogNativeSlugs(): string[] {
271
271
  // Deliberately ignore `visibility`: it is a rendered projection of disabledModels and account
272
272
  // selectors, so treating it as fresh availability would shrink the supported set between syncs.
273
273
  // visibleNativeSlugs applies the current disabledModels source of truth for public consumers.
274
+ return unique([...live, ...accountBound]);
275
+ }
276
+
277
+ export function listCatalogNativeSlugs(): string[] {
274
278
  // Ensure documented additions (e.g. gpt-5.3-codex-spark) appear even when the bundled catalog
275
279
  // predates the slug — mirrors nativeOpenAiSlugs() which already merges them for /v1/models.
276
- return unique([...live, ...accountBound, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]);
280
+ return unique([...catalogNativeSlugs(), ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]);
277
281
  }
@@ -151,6 +151,13 @@ interface CapturedProviderGather {
151
151
  readonly policy: CatalogProviderDiscoveryPolicySnapshot;
152
152
  readonly request: CapturedModelsRequest;
153
153
  readonly observedAuth?: ModelsAuthResolution;
154
+ /**
155
+ * Configured model ids this provider must keep even when live discovery omits
156
+ * them — combo targets that are also listed in providers.*.models (OCX-111).
157
+ * Combo-only ids (not in models[]) stay out of the public catalog and are
158
+ * synthesized for combo derivation instead (#1305).
159
+ */
160
+ readonly retainConfiguredModelIds?: ReadonlySet<string>;
154
161
  }
155
162
 
156
163
  interface GatherFlightCapture {
@@ -381,6 +388,7 @@ function captureProviderGather(
381
388
  name: string,
382
389
  configured: OcxProviderConfig,
383
390
  authResolver: ModelsAuthResolver,
391
+ retainConfiguredModelIds?: ReadonlySet<string>,
384
392
  ): CapturedProviderGather {
385
393
  const enriched = detachedClone(configured);
386
394
  enrichProviderFromRegistry(name, enriched);
@@ -422,18 +430,47 @@ function captureProviderGather(
422
430
  policy,
423
431
  request,
424
432
  ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
433
+ ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
434
+ ? { retainConfiguredModelIds }
435
+ : {}),
425
436
  });
426
437
  }
427
438
 
439
+ /** Model ids each provider must retain for combo catalog derivation (OCX-111). */
440
+ export function configuredComboTargetModelsByProvider(
441
+ config: Pick<OcxConfig, "combos">,
442
+ ): Map<string, ReadonlySet<string>> {
443
+ const byProvider = new Map<string, Set<string>>();
444
+ for (const id of listComboIds(config)) {
445
+ const combo = getCombo(config, id);
446
+ if (!combo) continue;
447
+ for (const target of combo.targets) {
448
+ let models = byProvider.get(target.provider);
449
+ if (!models) {
450
+ models = new Set();
451
+ byProvider.set(target.provider, models);
452
+ }
453
+ models.add(target.model);
454
+ }
455
+ }
456
+ return byProvider;
457
+ }
458
+
428
459
  function captureGatherFlight(
429
460
  config: OcxConfig,
430
461
  createAuthResolver: ModelsAuthResolverFactory,
431
462
  ): GatherFlightCapture {
432
463
  const providerAuthOutcomes: CatalogGatherProviderAuthOutcome[] = [];
433
464
  const authResolver = createAuthResolver(providerAuthOutcomes);
465
+ const comboTargetsByProvider = configuredComboTargetModelsByProvider(config);
434
466
  const providers = Object.entries(config.providers)
435
467
  .filter(([, provider]) => provider.disabled !== true)
436
- .map(([name, provider]) => captureProviderGather(name, provider, authResolver));
468
+ .map(([name, provider]) => captureProviderGather(
469
+ name,
470
+ provider,
471
+ authResolver,
472
+ comboTargetsByProvider.get(name),
473
+ ));
437
474
  const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
438
475
  return Object.freeze({
439
476
  discoveryPolicyIdentity: keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicySnapshots),
@@ -460,6 +497,9 @@ function captureGatherFlight(
460
497
  // It is the one member of a provider row that is legitimately a function,
461
498
  // so it is dropped here rather than allowed to break every encode.
462
499
  provider: omitProviderTransportExecutor(provider.provider),
500
+ // Combo retention is capture-time state, not a provider-row field. Two
501
+ // gathers that share providers but differ in combo targets must not join.
502
+ retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
463
503
  }))),
464
504
  discoveryPolicySnapshots,
465
505
  providers: Object.freeze(providers),
@@ -846,7 +886,9 @@ function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): st
846
886
  function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
847
887
  const metadata = plainRecord(item.metadata);
848
888
  const metadataCapabilities = metadata?.capabilities;
849
- const capabilityRecord = plainRecord(metadataCapabilities) ?? plainRecord(item.capabilities);
889
+ const capabilityRecord = plainRecord(metadataCapabilities)
890
+ ?? plainRecord(item.capabilities)
891
+ ?? plainRecord(item.features);
850
892
  const out = new Set<string>();
851
893
  for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
852
894
  for (const capability of normalizedStringList(list) ?? []) out.add(capability);
@@ -876,7 +918,9 @@ function modelInputModalities(
876
918
  capabilities: readonly string[] | undefined,
877
919
  ): string[] | undefined {
878
920
  const metadata = plainRecord(item.metadata);
879
- const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
921
+ const capabilityRecord = plainRecord(metadata?.capabilities)
922
+ ?? plainRecord(item.capabilities)
923
+ ?? plainRecord(item.features);
880
924
  const explicit = normalizedStringList(
881
925
  item.input_modalities
882
926
  ?? item.modalities
@@ -903,7 +947,9 @@ function modelInputModalities(
903
947
  if (inferred.length > 0) return [...new Set(inferred)];
904
948
  }
905
949
  if (capabilityRecord?.vision === false) return ["text"];
906
- if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
950
+ if (capabilityRecord?.vision === true || capabilities?.some(value => (
951
+ value === "vision" || value === "image-input" || value === "image_input"
952
+ ))) {
907
953
  return ["text", "image"];
908
954
  }
909
955
  return undefined;
@@ -923,7 +969,16 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
923
969
  item.max_context_length,
924
970
  );
925
971
  const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
926
- const rawReasoningEfforts = capabilityRecord?.reasoning_effort ?? item.reasoning_efforts;
972
+ // Some OpenAI-compatible catalogs expose the selectable ladder under
973
+ // `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key.
974
+ // Treat both as model metadata: otherwise a valid upstream capability disappears
975
+ // before client exporters (including omp) can advertise it.
976
+ const reasoningParameters = plainRecord(item.reasoning_parameters)
977
+ ?? plainRecord(metadata?.reasoning_parameters)
978
+ ?? plainRecord(capabilityRecord?.reasoning_parameters);
979
+ const rawReasoningEfforts = capabilityRecord?.reasoning_effort
980
+ ?? item.reasoning_efforts
981
+ ?? reasoningParameters?.efforts;
927
982
  const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24);
928
983
  const reasoningEfforts = listedReasoningEfforts
929
984
  ? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
@@ -1004,6 +1059,30 @@ async function fetchProviderModelsWithAuth(
1004
1059
  provider: name,
1005
1060
  ...catalogHintsFromProviderConfig(name, prov, id, contextCap),
1006
1061
  }));
1062
+ const withConfiguredRetention = (
1063
+ models: CatalogModel[],
1064
+ options?: { retainComboTargets?: boolean; warnDrops?: boolean },
1065
+ ): CatalogModel[] => {
1066
+ const { models: merged, droppedConfiguredIds } = mergeConfiguredModelsIntoLiveCatalog({
1067
+ name,
1068
+ provider: prov,
1069
+ models,
1070
+ configured,
1071
+ retainConfiguredModelIds: captured.retainConfiguredModelIds,
1072
+ contextCap,
1073
+ seedVertexDefault,
1074
+ retainComboTargets: options?.retainComboTargets,
1075
+ });
1076
+ if (
1077
+ options?.warnDrops === true
1078
+ && droppedConfiguredIds.length > 0
1079
+ && name !== OPENAI_API_PROVIDER_ID
1080
+ && !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)
1081
+ ) {
1082
+ warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
1083
+ }
1084
+ return merged;
1085
+ };
1007
1086
  // Static catalogs never need an OAuth refresh or an upstream model request. Clear any
1008
1087
  // discovery failure left by an older live configuration even when the account is logged out.
1009
1088
  if (prov.liveModels === false) {
@@ -1047,12 +1126,17 @@ async function fetchProviderModelsWithAuth(
1047
1126
  // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
1048
1127
  const cachedCursor = getFreshCached(name, ttlMs);
1049
1128
  if (cachedCursor) {
1050
- return observed(applyConfigHintsToCachedModels(name, prov, cachedCursor), "authoritative");
1129
+ return observed(
1130
+ withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor)),
1131
+ "authoritative",
1132
+ );
1051
1133
  }
1052
1134
  if (isModelsFetchCoolingDown(name)) {
1053
1135
  const cooling = getStaleCached(name);
1054
1136
  return observed(
1055
- cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
1137
+ withConfiguredRetention(
1138
+ cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
1139
+ ),
1056
1140
  "degraded",
1057
1141
  );
1058
1142
  }
@@ -1060,10 +1144,14 @@ async function fetchProviderModelsWithAuth(
1060
1144
  if (liveResult.ok) {
1061
1145
  const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
1062
1146
  const result = available.length > 0 ? available : configured;
1063
- // Count what discovery actually returned, not the configured rows we fall back to.
1064
- if (!setCached(name, result, Date.now(), cacheGeneration)) return observed(configured, "degraded");
1147
+ // Cache the discovery-filtered roster without combo retention so a later
1148
+ // gather can re-apply the current capture's retain set on read.
1149
+ const forCache = withConfiguredRetention(result, { retainComboTargets: false });
1150
+ if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
1151
+ return observed(withConfiguredRetention(configured), "degraded");
1152
+ }
1065
1153
  markProviderDiscoveryOk(name, liveResult.models.length);
1066
- return observed(result, "authoritative");
1154
+ return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
1067
1155
  }
1068
1156
  if (isCurrentCacheGeneration()) {
1069
1157
  markModelsFetchFailure(name);
@@ -1074,7 +1162,9 @@ async function fetchProviderModelsWithAuth(
1074
1162
  }
1075
1163
  const staleCursor = getStaleCached(name);
1076
1164
  return observed(
1077
- staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured,
1165
+ withConfiguredRetention(
1166
+ staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured,
1167
+ ),
1078
1168
  "degraded",
1079
1169
  );
1080
1170
  }
@@ -1090,7 +1180,9 @@ async function fetchProviderModelsWithAuth(
1090
1180
  const fresh = getFreshCached(name, ttlMs);
1091
1181
  if (fresh) {
1092
1182
  return observed(
1093
- withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)),
1183
+ withConfiguredRetention(
1184
+ withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)),
1185
+ ),
1094
1186
  "authoritative",
1095
1187
  ); // dedups Codex's frequent /v1/models polling within the TTL
1096
1188
  }
@@ -1099,9 +1191,11 @@ async function fetchProviderModelsWithAuth(
1099
1191
  // fetch timeout on every catalog poll — the dashboard polls this path per page load.
1100
1192
  const stale = getStaleCached(name);
1101
1193
  return observed(
1102
- stale
1103
- ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
1104
- : failedDiscoveryConfigured,
1194
+ withConfiguredRetention(
1195
+ stale
1196
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
1197
+ : failedDiscoveryConfigured,
1198
+ ),
1105
1199
  "degraded",
1106
1200
  );
1107
1201
  }
@@ -1114,7 +1208,11 @@ async function fetchProviderModelsWithAuth(
1114
1208
  failure: ProviderModelDiscoveryFailure,
1115
1209
  ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
1116
1210
  if (!isCurrentCacheGeneration()) {
1117
- return { models: failedDiscoveryConfigured, fallback: "configured", shouldLog: false };
1211
+ return {
1212
+ models: withConfiguredRetention(failedDiscoveryConfigured),
1213
+ fallback: "configured",
1214
+ shouldLog: false,
1215
+ };
1118
1216
  }
1119
1217
  // Decide logging BEFORE recording the new status, so we can compare against the prior one and
1120
1218
  // suppress an identical repeated failure (#395 log flood). The failure stays observable via the
@@ -1124,9 +1222,11 @@ async function fetchProviderModelsWithAuth(
1124
1222
  markProviderDiscoveryFailed(name, failure);
1125
1223
  const stale = getStaleCached(name);
1126
1224
  return {
1127
- models: stale
1128
- ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
1129
- : failedDiscoveryConfigured,
1225
+ models: withConfiguredRetention(
1226
+ stale
1227
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
1228
+ : failedDiscoveryConfigured,
1229
+ ),
1130
1230
  fallback: stale ? "stale" : "configured",
1131
1231
  shouldLog,
1132
1232
  };
@@ -1202,9 +1302,12 @@ async function fetchProviderModelsWithAuth(
1202
1302
  ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
1203
1303
  ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
1204
1304
  }, contextCap));
1205
- if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
1305
+ const forCache = withConfiguredRetention(live, { retainComboTargets: false });
1306
+ if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
1307
+ return observed(withConfiguredRetention(configured), "degraded");
1308
+ }
1206
1309
  markProviderDiscoveryOk(name, live.length);
1207
- return observed(live, "authoritative");
1310
+ return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
1208
1311
  }
1209
1312
  const extracted = extractProviderModelItems(bounded.value, discovery);
1210
1313
  if (!extracted.ok) {
@@ -1236,37 +1339,24 @@ async function fetchProviderModelsWithAuth(
1236
1339
  // Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
1237
1340
  // `live`; otherwise configured entries would be reported as discovered ones.
1238
1341
  const liveModelCount = live.length;
1239
- const liveIds = new Set(live.map(m => m.id));
1240
- // Dated-release aliases (Anthropic pattern): older models may appear in the live catalog
1241
- // ONLY under their dated id (claude-haiku-4-5-20251001) while the config names the
1242
- // API-valid alias (claude-haiku-4-5). Such aliases are real, callable models keep them
1243
- // in the authoritative catalog (alias id, hints from the dated live entry) instead of
1244
- // dropping them and warning on every poll.
1245
- const droppedConfiguredIds: string[] = [];
1246
- for (const m of configured) {
1247
- if (liveIds.has(m.id)) continue;
1248
- const dated = live.find(l => isDatedVariantId(l.id, m.id));
1249
- if (dated) {
1250
- // Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
1251
- live.push(applyProviderConfigHints(name, prov, { ...dated, id: m.id }, contextCap));
1252
- } else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
1253
- live.push(m);
1254
- } else {
1255
- droppedConfiguredIds.push(m.id);
1256
- }
1257
- }
1258
- if (live.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
1342
+ // Dated-release aliases + configured retention (compat allow-list, combo targets,
1343
+ // Vertex default). Cache without combo retention so a later gather re-applies the
1344
+ // current capture's retain set on read (warm-cache OCX-111 / #1308).
1345
+ const forCache = withConfiguredRetention(live, { retainComboTargets: false });
1346
+ const returned = withConfiguredRetention(forCache, { warnDrops: true });
1347
+ const droppedConfiguredIds = configured
1348
+ .map(model => model.id)
1349
+ .filter(id => !returned.some(model => model.id === id));
1350
+ if (returned.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
1259
1351
  console.warn(
1260
1352
  `[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
1261
1353
  );
1262
- } else if (droppedConfiguredIds.length > 0
1263
- && name !== OPENAI_API_PROVIDER_ID
1264
- && !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
1265
- warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
1266
1354
  }
1267
- if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
1355
+ if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
1356
+ return observed(withConfiguredRetention(configured), "degraded");
1357
+ }
1268
1358
  markProviderDiscoveryOk(name, liveModelCount);
1269
- return observed(live, "authoritative");
1359
+ return observed(returned, "authoritative");
1270
1360
  } catch (error) {
1271
1361
  if (error instanceof ProviderOutboundPolicyError) {
1272
1362
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
@@ -1313,6 +1403,60 @@ export function shouldRetainConfiguredProviderModel(providerName: string, modelI
1313
1403
  return false;
1314
1404
  }
1315
1405
 
1406
+ /**
1407
+ * Fold dated-release aliases and retain configured rows that must survive an
1408
+ * authoritative live roster (compatibility allow-list, combo targets, Vertex
1409
+ * default). Used on every discovery return — live, fresh cache, stale, and
1410
+ * failure fallback — so a warm cache captured before a combo existed still
1411
+ * surfaces the configured target (OCX-111 / #1308).
1412
+ *
1413
+ * Cache writes should pass `retainComboTargets: false` so combo retention is
1414
+ * re-applied on read against the current capture, not frozen into the TTL entry.
1415
+ */
1416
+ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
1417
+ name: string;
1418
+ provider: OcxProviderConfig;
1419
+ models: readonly CatalogModel[];
1420
+ configured: readonly CatalogModel[];
1421
+ retainConfiguredModelIds?: ReadonlySet<string>;
1422
+ contextCap?: number;
1423
+ seedVertexDefault?: boolean;
1424
+ retainComboTargets?: boolean;
1425
+ }): { models: CatalogModel[]; droppedConfiguredIds: string[] } {
1426
+ const {
1427
+ name,
1428
+ provider: prov,
1429
+ configured,
1430
+ retainConfiguredModelIds,
1431
+ contextCap,
1432
+ seedVertexDefault,
1433
+ retainComboTargets = true,
1434
+ } = opts;
1435
+ const out = [...opts.models];
1436
+ const present = new Set(out.map(model => model.id));
1437
+ const droppedConfiguredIds: string[] = [];
1438
+ for (const candidate of configured) {
1439
+ if (present.has(candidate.id)) continue;
1440
+ const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
1441
+ if (dated) {
1442
+ out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap));
1443
+ present.add(candidate.id);
1444
+ continue;
1445
+ }
1446
+ if (
1447
+ seedVertexDefault === true
1448
+ || shouldRetainConfiguredProviderModel(name, candidate.id)
1449
+ || (retainComboTargets && retainConfiguredModelIds?.has(candidate.id) === true)
1450
+ ) {
1451
+ out.push(candidate);
1452
+ present.add(candidate.id);
1453
+ continue;
1454
+ }
1455
+ droppedConfiguredIds.push(candidate.id);
1456
+ }
1457
+ return { models: out, droppedConfiguredIds };
1458
+ }
1459
+
1316
1460
  export function filterCatalogVisibleModels(
1317
1461
  models: CatalogModel[],
1318
1462
  config: Pick<OcxConfig, "disabledModels" | "providers">,
@@ -2,10 +2,10 @@
2
2
  // Public surface preserved exactly; importers keep using "src/codex/catalog".
3
3
  export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
4
4
  export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
5
- export { CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
5
+ export { CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
6
6
  export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
7
7
  export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
8
- export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember } from "./catalog/provider-fetch";
8
+ export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
9
9
  export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
10
10
  export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
11
11
  export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";