@bitkyc08/opencodex 2.55.0-preview.20260914 → 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-DH2PUHqr.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,304 @@
1
+ import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
+ import { initialModelSelectionPending } from "../../providers/initial-model-selection";
3
+ import { execFileSync } from "node:child_process";
4
+ import { createHash, createHmac, randomBytes } from "node:crypto";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
6
+ import { delimiter, dirname, join, resolve } from "node:path";
7
+ import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
8
+ import { resolveProviderApiKey } from "../../providers/key-store";
9
+ import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
10
+ import {
11
+ clearModelCache,
12
+ clearProviderDiscoveryStatus,
13
+ captureModelCacheGeneration,
14
+ DEFAULT_MODEL_CACHE_TTL_MS,
15
+ getFreshCached,
16
+ getStaleCached,
17
+ isModelsFetchCoolingDown,
18
+ isModelCacheGenerationCurrent,
19
+ markModelsFetchFailure,
20
+ markProviderDiscoveryFailed,
21
+ markProviderDiscoveryOk,
22
+ shouldLogDiscoveryFailure,
23
+ setCached,
24
+ type ProviderModelDiscoveryFailure,
25
+ } from "../model-cache";
26
+ import {
27
+ buildModelsRequest,
28
+ getValidAccessTokenSnapshot,
29
+ observeActiveOAuthAccessToken,
30
+ resolveModelsAuthToken,
31
+ type OAuthActiveTokenObservation,
32
+ } from "../../oauth";
33
+ import type { OcxConfig, OcxProviderConfig } from "../../types";
34
+ import { modelInList } from "../../types";
35
+ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
36
+ import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
37
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
38
+ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
39
+ import {
40
+ captureFastPolicyAuthority,
41
+ fastPolicyForModel,
42
+ serviceTierSupportFromPolicy,
43
+ } from "../../providers/service-tier";
44
+ import type { FastPolicyAuthority } from "../../providers/fastwire";
45
+ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
46
+ import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
47
+ import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
48
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
49
+ import { effectiveModelAliases } from "../../providers/default-aliases";
50
+ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
51
+ import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
52
+ import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
53
+ import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
54
+ import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
55
+ import { fetchQoderModels } from "../../adapters/qoder/live-models";
56
+ import { resolveQoderProfile } from "../../adapters/qoder/profiles";
57
+ import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
58
+ import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
59
+ import {
60
+ COMBO_NAMESPACE,
61
+ comboModelId,
62
+ getCombo,
63
+ listComboIds,
64
+ quotaInactiveReason,
65
+ targetKey,
66
+ } from "../../combos";
67
+ import type { NormalizedComboConfig } from "../../combos/types";
68
+ import {
69
+ ProviderOutboundPolicyError,
70
+ providerOutboundGet,
71
+ providerOutboundPost,
72
+ providerRedirectError,
73
+ } from "../../lib/provider-outbound";
74
+ import { redactSecretString } from "../../lib/redact";
75
+ import {
76
+ extractProviderModelItems,
77
+ isRegistryModelDiscoveryUrl,
78
+ readBoundedDiscoveryJson,
79
+ resolveProviderModelDiscovery,
80
+ type ModelDiscoveryResponseFailure,
81
+ type ProviderModelsApiItem,
82
+ type ResolvedProviderModelDiscovery,
83
+ } from "../../providers/model-discovery";
84
+ import { extractGoogleAiStudioModelItems } from "../../providers/google-ai-studio-model-discovery";
85
+ import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show";
86
+ import upstreamModelsSnapshot from "../data/upstream-models.json";
87
+ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
88
+ import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
89
+ import type { CatalogModel } from "./parsing";
90
+ import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
91
+ import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
92
+ import type { ComboCatalogOmission } from "./aggregation";
93
+ import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
94
+ import type {
95
+ CatalogAdmissionSnapshot,
96
+ CatalogDiscoveryPolicyField,
97
+ CatalogGatherAuthorityIdentity,
98
+ CatalogProviderDiscoveryPolicySnapshot,
99
+ CatalogProcessLocalEvidence,
100
+ CatalogSourceEvidence,
101
+ CatalogTrustedOpenAiApiPolicySnapshot,
102
+ } from "../convergence-types";
103
+ import { CALLABLE_CONFIGURED_COMPATIBILITY_MODELS, applyProviderConfigHints } from "./model-hints";
104
+
105
+ const DATED_VARIANT_YYYYMMDD = /^(\d{4})(\d{2})(\d{2})$/;
106
+ const DATED_VARIANT_YYMMDD = /^(2\d)(\d{2})(\d{2})$/;
107
+ const DATED_VARIANT_MMDD_OR_YYMM = /^(\d{2})(\d{2})$/;
108
+
109
+ /** Whether a Gregorian year contains February 29th. */
110
+ function isLeapYear(year: number): boolean {
111
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
112
+ }
113
+
114
+ /**
115
+ * Whether a month/day pair exists in the given year. Without a year, February 29th is
116
+ * accepted because it occurs in at least one calendar year.
117
+ */
118
+ function isValidCalendarDate(year: number | undefined, month: number, day: number): boolean {
119
+ if (year !== undefined && (year < 1 || year > 9999)) return false;
120
+ if (month < 1 || month > 12 || day < 1) return false;
121
+ const daysInMonth = [
122
+ 31, year === undefined || isLeapYear(year) ? 29 : 28, 31, 30, 31, 30,
123
+ 31, 31, 30, 31, 30, 31,
124
+ ];
125
+ return day <= daysInMonth[month - 1]!;
126
+ }
127
+
128
+ /**
129
+ * Release-date suffixes providers actually publish: `YYYYMMDD` (`-20251001`), `YYMMDD`
130
+ * (`-260806`), `MMDD` (`-0813`) and `YYMM` (`-2512`). A `\d{8}`-only rule matched none of
131
+ * the dated ids on a real multi-provider install, so DeepSeek, Kimi, Mistral, Qwen and
132
+ * Solar aliases all fell through to `droppedConfiguredIds` (#3024).
133
+ *
134
+ * Calendar validation rejects impossible month-end and leap-day values as well as ordinary
135
+ * numeric suffixes such as `-2048`, `-4096` and `-8192`. `-1024` is the one irreducible
136
+ * collision — it is a valid `MMDD` (October 24th) — so it reads as dated. That is a known,
137
+ * accepted cost; the test table pins it so it cannot become a surprise later.
138
+ *
139
+ * Hyphenated ISO suffixes (`-2024-08-06`, `-05-06`) are deliberately out of scope: a
140
+ * hyphenated suffix is ambiguous against ordinary name segments and needs its own call.
141
+ */
142
+ function isDatedVariantSuffix(suffix: string): boolean {
143
+ const yyyyMmDd = DATED_VARIANT_YYYYMMDD.exec(suffix);
144
+ if (yyyyMmDd) {
145
+ return isValidCalendarDate(
146
+ Number(yyyyMmDd[1]), Number(yyyyMmDd[2]), Number(yyyyMmDd[3]),
147
+ );
148
+ }
149
+
150
+ const yyMmDd = DATED_VARIANT_YYMMDD.exec(suffix);
151
+ if (yyMmDd) {
152
+ return isValidCalendarDate(
153
+ 2000 + Number(yyMmDd[1]), Number(yyMmDd[2]), Number(yyMmDd[3]),
154
+ );
155
+ }
156
+
157
+ const mmDdOrYyMm = DATED_VARIANT_MMDD_OR_YYMM.exec(suffix);
158
+ if (!mmDdOrYyMm) return false;
159
+ const first = Number(mmDdOrYyMm[1]);
160
+ const second = Number(mmDdOrYyMm[2]);
161
+ return isValidCalendarDate(undefined, first, second)
162
+ || (first >= 20 && first <= 29 && second >= 1 && second <= 12);
163
+ }
164
+
165
+ /** Whether `liveId` is a supported dated release of the configured base id. */
166
+ export function isDatedVariantId(liveId: string, configuredId: string): boolean {
167
+ if (!liveId.startsWith(`${configuredId}-`)) return false;
168
+ return isDatedVariantSuffix(liveId.slice(configuredId.length + 1));
169
+ }
170
+
171
+ export const lastDropWarnSignature = new Map<string, string>();
172
+ let lastWarningReconciledGeneration = 0;
173
+
174
+ export function reconcileProviderFetchWarnings(generation: number): number {
175
+ if (generation <= lastWarningReconciledGeneration) return 0;
176
+ const removed = lastDropWarnSignature.size;
177
+ lastDropWarnSignature.clear();
178
+ lastWarningReconciledGeneration = generation;
179
+ return removed;
180
+ }
181
+ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds: string[]): void {
182
+ const signature = [...droppedConfiguredIds].sort().join(",");
183
+ if (lastDropWarnSignature.get(name) === signature) return;
184
+ lastDropWarnSignature.set(name, signature);
185
+ console.warn(
186
+ `[opencodex] Provider model discovery for "${name}" omitted configured model ids; dropping them from the authoritative live catalog: ${droppedConfiguredIds.join(", ")}.`,
187
+ );
188
+ }
189
+ export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
190
+ if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
191
+ // xAI /models advertises both the dated deployment and this floating alias.
192
+ // Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
193
+ if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
194
+ return true;
195
+ }
196
+
197
+ export function shouldRetainConfiguredProviderModel(
198
+ providerName: string,
199
+ modelId: string,
200
+ prov?: OcxProviderConfig,
201
+ ): boolean {
202
+ if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
203
+ if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
204
+ if (modelInList(prov?.retainModels, modelId)) return true;
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * Fold dated-release aliases and retain configured rows that must survive an
210
+ * authoritative live roster (compatibility allow-list, combo targets, Vertex
211
+ * default). Used on every discovery return — live, fresh cache, stale, and
212
+ * failure fallback — so a warm cache captured before a combo existed still
213
+ * surfaces the configured target (OCX-111 / #1308).
214
+ *
215
+ * Cache writes should pass `retainComboTargets: false` so combo retention is
216
+ * re-applied on read against the current capture, not frozen into the TTL entry.
217
+ */
218
+ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
219
+ name: string;
220
+ provider: OcxProviderConfig;
221
+ models: readonly CatalogModel[];
222
+ configured: readonly CatalogModel[];
223
+ retainConfiguredModelIds?: ReadonlySet<string>;
224
+ contextCap?: number;
225
+ seedVertexDefault?: boolean;
226
+ retainComboTargets?: boolean;
227
+ metadataModelIdCaseFold?: boolean;
228
+ }): { models: CatalogModel[]; droppedConfiguredIds: string[] } {
229
+ const {
230
+ name,
231
+ provider: prov,
232
+ configured,
233
+ retainConfiguredModelIds,
234
+ contextCap,
235
+ seedVertexDefault,
236
+ retainComboTargets = true,
237
+ metadataModelIdCaseFold,
238
+ } = opts;
239
+ const out = [...opts.models];
240
+ const present = new Set(out.map(model => model.id));
241
+ const droppedConfiguredIds: string[] = [];
242
+ for (const candidate of configured) {
243
+ if (present.has(candidate.id)) continue;
244
+ const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
245
+ if (dated) {
246
+ out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold));
247
+ present.add(candidate.id);
248
+ continue;
249
+ }
250
+ if (
251
+ seedVertexDefault === true
252
+ || shouldRetainConfiguredProviderModel(name, candidate.id, prov)
253
+ || (retainComboTargets && retainConfiguredModelIds?.has(candidate.id) === true)
254
+ ) {
255
+ out.push(candidate);
256
+ present.add(candidate.id);
257
+ continue;
258
+ }
259
+ droppedConfiguredIds.push(candidate.id);
260
+ }
261
+ return { models: out, droppedConfiguredIds };
262
+ }
263
+
264
+ export function filterCatalogVisibleModels(
265
+ models: CatalogModel[],
266
+ config: Pick<OcxConfig, "disabledModels" | "providers">,
267
+ ): CatalogModel[] {
268
+ const disabled = new Set(config.disabledModels ?? []);
269
+ const allowByProvider = new Map<string, Set<string>>();
270
+ for (const [name, prov] of Object.entries(config.providers)) {
271
+ const sel = prov.selectedModels;
272
+ // Keyed the way `sync.ts` keys the same list, so a slash-bearing native id and
273
+ // the encoded slug the Codex picker displays are one entry rather than two. A
274
+ // bare `Set(sel)` matched only the native form, so an allowlist written from the
275
+ // displayed slug — which `ocx models remove` also accepts — hid every model it
276
+ // was meant to keep.
277
+ //
278
+ // The key is deliberately lossy: `p/a/b` and `p/a-b` collapse to one entry, so a
279
+ // provider publishing both spellings has them selected together. That is a real
280
+ // limitation, pinned by the tests below and tracked as a follow-up; it is NOT
281
+ // fixed here. Resolving selections against the current roster instead was tried
282
+ // and rejected — the roster is an incomplete dictionary (live discovery can omit
283
+ // a published id), so it produces the same over-grant while additionally
284
+ // disagreeing with the `slugEquivalenceKey` contract `sync.ts` uses at merge time.
285
+ // Two catalog stages with different equivalence relations is the exact bug class
286
+ // this change exists to remove.
287
+ if (Array.isArray(sel) && sel.length > 0) {
288
+ allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model)))));
289
+ }
290
+ }
291
+ return models.filter(m => {
292
+ if (initialModelSelectionPending(config.providers[m.provider])) return false;
293
+ const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
294
+ // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
295
+ for (const stored of disabled) {
296
+ // Combo management stores the public alias, while canonical `combo/<id>` references
297
+ // remain valid for backward compatibility through slugEquals below.
298
+ if (m.alias !== undefined && stored === catalogModelSlug(m) && !nativeAlias) return false;
299
+ if (slugEquals(stored, m.provider, m.id)) return false;
300
+ }
301
+ const allow = allowByProvider.get(m.provider);
302
+ return !allow || allow.has(slugEquivalenceKey(routedSlug(m.provider, m.id)));
303
+ });
304
+ }