@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,375 @@
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 { applyProviderConfigHints, configuredAutoCompactTokenLimit, positiveSafeInteger } from "./model-hints";
104
+
105
+ /** Model ids each provider must retain for combo catalog derivation (OCX-111). */
106
+ export function configuredComboTargetModelsByProvider(
107
+ config: Pick<OcxConfig, "combos">,
108
+ ): Map<string, ReadonlySet<string>> {
109
+ const byProvider = new Map<string, Set<string>>();
110
+ for (const id of listComboIds(config)) {
111
+ const combo = getCombo(config, id);
112
+ if (!combo) continue;
113
+ for (const target of combo.targets) {
114
+ let models = byProvider.get(target.provider);
115
+ if (!models) {
116
+ models = new Set();
117
+ byProvider.set(target.provider, models);
118
+ }
119
+ models.add(target.model);
120
+ }
121
+ }
122
+ return byProvider;
123
+ }
124
+ /**
125
+ * Last-resort context window for combo member synthesis when discovery,
126
+ * provider config, and an enabled Context cap all omit one. Matches the
127
+ * catalog entry default in `normalizeRoutedCatalogEntry` so incomplete live
128
+ * rows still catalog. An enabled Context cap is the operator-facing window,
129
+ * not a clamp on this placeholder.
130
+ */
131
+ const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000;
132
+
133
+ interface ComboCatalogMemberFallback {
134
+ readonly contextWindow?: number;
135
+ /** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
136
+ readonly maxInputTokens?: number;
137
+ readonly maxOutputTokens?: number;
138
+ readonly autoCompactTokenLimit?: number;
139
+ readonly inputModalities?: readonly string[];
140
+ readonly reasoningEfforts?: readonly string[];
141
+ }
142
+
143
+ /**
144
+ * Ladder advertised for a combo member whose vendor metadata says it reasons but
145
+ * carries no explicit ladder (Claude, Grok). Codex needs a non-empty ladder to show
146
+ * the effort control; the routed adapters clamp to the real upstream top rung.
147
+ */
148
+ const ROUTED_COMBO_MEMBER_REASONING_EFFORTS: readonly string[] = ["low", "medium", "high", "xhigh", "max"];
149
+
150
+ /**
151
+ * Vendor-table lookup tolerant of point releases and date pins. Configured combo
152
+ * targets often name a variant the table does not carry (`claude-fable-5-1`,
153
+ * `claude-opus-4-5-20251101`); the base family row still describes its modality
154
+ * and reasoning capability, so fall back to it before giving up.
155
+ */
156
+ function comboMemberVendorMetadata(provider: string, modelId: string): ModelMetadata | undefined {
157
+ const exact = getModelMetadataCaseInsensitive(provider, modelId);
158
+ if (exact) return exact;
159
+ let candidate = modelId.replace(/\[[^\]]*\]$/, "");
160
+ while (true) {
161
+ const trimmed = candidate.replace(/-\d+$/, "");
162
+ if (trimmed === candidate || !trimmed.includes("-")) return undefined;
163
+ const hit = getModelMetadataCaseInsensitive(provider, trimmed);
164
+ if (hit) return hit;
165
+ candidate = trimmed;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Combo members are usually thin discovery rows (id + context window). Without a
171
+ * capability source the combo intersection collapses to text-only / no effort ladder,
172
+ * and the Codex app then refuses image attachments and hides the effort picker for
173
+ * every Claude combo. The generated vendor table knows both, so use it as the
174
+ * last-resort fallback when the caller supplied none.
175
+ *
176
+ * `ModelMetadata.maxTokens` is the OUTPUT ceiling, so it fills `maxOutputTokens`.
177
+ * Mapping it onto `maxInputTokens` would be read by the combo intersection
178
+ * (`aggregation.ts` `Math.min` over member input ceilings) as a 128k input limit and
179
+ * shrink a 1M Claude combo window to 128k, taking autoCompactTokenLimit down with it.
180
+ */
181
+ function vendorMetadataComboFallback(target: { provider: string; model: string }): ComboCatalogMemberFallback | undefined {
182
+ const metadataProvider = resolveMetadataProvider(target.provider);
183
+ // Custom OpenAI-compatible routes commonly retain the canonical OpenAI model id
184
+ // while using a provider name that has no metadata alias. Reuse only its effort
185
+ // ladder below; context/modality rows remain provider-owned.
186
+ const metadata = metadataProvider
187
+ ? comboMemberVendorMetadata(metadataProvider, target.model)
188
+ : comboMemberVendorMetadata("openai", target.model);
189
+ if (!metadata) return undefined;
190
+ return {
191
+ ...(metadataProvider && typeof metadata.contextWindow === "number" && metadata.contextWindow > 0
192
+ ? { contextWindow: metadata.contextWindow }
193
+ : {}),
194
+ ...(metadataProvider && typeof metadata.maxTokens === "number" && metadata.maxTokens > 0
195
+ ? { maxOutputTokens: metadata.maxTokens }
196
+ : {}),
197
+ ...(metadataProvider && Array.isArray(metadata.input) && metadata.input.length > 0
198
+ ? { inputModalities: [...metadata.input] }
199
+ : {}),
200
+ ...(metadata.reasoning === true ? { reasoningEfforts: [...ROUTED_COMBO_MEMBER_REASONING_EFFORTS] } : {}),
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Resolve a combo target to a catalog member for derivation.
206
+ * Prefer discovery metadata; when the target is missing from the gather map or
207
+ * lacks a positive contextWindow, synthesize from the (registry-enriched)
208
+ * provider config so combos remain catalogued when targets are configured but
209
+ * discovery metadata is incomplete. Disabled providers stay unresolved.
210
+ * When hints still omit contextWindow, prefer known maxInputTokens, else the
211
+ * enabled Context cap, else COMBO_MEMBER_CONTEXT_FALLBACK so a live row
212
+ * without ctx does not drop the whole combo from the public catalog.
213
+ */
214
+ export function resolveComboCatalogMember(
215
+ target: { provider: string; model: string },
216
+ memberByKey: ReadonlyMap<string, CatalogModel>,
217
+ providers: ReadonlyMap<string, OcxProviderConfig>,
218
+ contextCap?: number,
219
+ callerFallback?: ComboCatalogMemberFallback,
220
+ metadataModelIdCaseFold?: boolean,
221
+ ): CatalogModel | undefined {
222
+ const existing = memberByKey.get(targetKey(target));
223
+ const prov = providers.get(target.provider);
224
+ const fallback = callerFallback ?? vendorMetadataComboFallback(target);
225
+ // Disabled providers never contribute members — even a complete discovery row
226
+ // is unusable for catalog derivation while the provider is off.
227
+ if (prov?.disabled === true) return undefined;
228
+
229
+ const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
230
+ const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
231
+ ? member.contextWindow
232
+ : undefined;
233
+ const addMaxInput = fallback !== undefined && contextWindow !== undefined
234
+ && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
235
+ const addMaxOutput = fallback !== undefined
236
+ && typeof fallback.maxOutputTokens === "number"
237
+ && fallback.maxOutputTokens > 0
238
+ && !(typeof member.maxOutputTokens === "number" && member.maxOutputTokens > 0);
239
+ const effectiveMaxInput = addMaxInput
240
+ ? Math.min(fallback?.maxInputTokens ?? contextWindow!, contextWindow!)
241
+ : member.maxInputTokens;
242
+ const softCandidates = [member.autoCompactTokenLimit, fallback?.autoCompactTokenLimit]
243
+ .filter((value): value is number => typeof value === "number" && value > 0);
244
+ const autoCompactTokenLimit = contextWindow !== undefined && softCandidates.length > 0
245
+ ? clampAutoCompactTokenLimit(contextWindow, effectiveMaxInput, Math.min(...softCandidates))
246
+ : member.autoCompactTokenLimit;
247
+ const adjustAutoCompact = autoCompactTokenLimit !== member.autoCompactTokenLimit;
248
+ const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
249
+ && fallback?.inputModalities !== undefined;
250
+ const addReasoning = member.reasoningEfforts === undefined
251
+ && fallback?.reasoningEfforts !== undefined;
252
+ if (!addMaxInput && !addMaxOutput && !adjustAutoCompact && !addModalities && !addReasoning) return member;
253
+ return {
254
+ ...member,
255
+ // Never claim a larger input budget than the window, and prefer the model's own
256
+ // measured ceiling when the fallback carries one.
257
+ ...(addMaxInput ? { maxInputTokens: effectiveMaxInput } : {}),
258
+ ...(addMaxOutput ? { maxOutputTokens: fallback!.maxOutputTokens } : {}),
259
+ ...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
260
+ ...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}),
261
+ ...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}),
262
+ };
263
+ };
264
+
265
+ // Complete live/configured rows still honour providerContextCaps so a high
266
+ // discovery window cannot outrun an operator-configured cap. Native-alias
267
+ // fallback metadata may fill only capability gaps; it never raises an explicit
268
+ // discovered/configured context window.
269
+ if (
270
+ existing
271
+ && typeof existing.contextWindow === "number"
272
+ && existing.contextWindow > 0
273
+ ) {
274
+ // Live discovery can explicitly say text-only even when configured routing
275
+ // supplies a vision sidecar. Apply the same provider hints used for thin
276
+ // rows before deriving a combo from this complete row.
277
+ const hinted = prov && isModelVisionSidecarConsumer(prov, existing.id)
278
+ ? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold)
279
+ : existing;
280
+ const capped = applyProviderContextCap(hinted.contextWindow, contextCap);
281
+ if (capped === undefined || capped === existing.contextWindow) {
282
+ return withFallbackMetadata(hinted);
283
+ }
284
+ const maxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
285
+ ? Math.min(hinted.maxInputTokens, capped)
286
+ : Math.min(fallback?.maxInputTokens ?? capped, capped);
287
+ return withFallbackMetadata({
288
+ ...hinted,
289
+ contextWindow: capped,
290
+ maxInputTokens: maxInput,
291
+ contextCap,
292
+ contextCapped: true as const,
293
+ });
294
+ }
295
+
296
+ const base: CatalogModel = existing ?? {
297
+ id: target.model,
298
+ provider: target.provider,
299
+ };
300
+ const hinted = prov
301
+ ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold)
302
+ : base;
303
+ const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
304
+ ? hinted.contextWindow
305
+ : undefined;
306
+ const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
307
+ ? hinted.maxInputTokens
308
+ : (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0
309
+ ? base.maxInputTokens
310
+ : undefined);
311
+ // Kept OUT of knownMaxInput on purpose: that value doubles as a context-window fallback
312
+ // below, and a native alias whose input ceiling (922k) is lower than its window (1.05M)
313
+ // would otherwise shrink the advertised window to the input limit.
314
+ const fallbackMaxInput = existing || prov ? fallback?.maxInputTokens : undefined;
315
+ // Real discovery/config values win. A native alias is the next fallback tier.
316
+ // The generic 128k/text synthesis from #1305 remains the final fallback.
317
+ const fallbackContext = existing || prov ? fallback?.contextWindow : undefined;
318
+ const uncappedContext = hintedContext
319
+ ?? knownMaxInput
320
+ ?? fallbackContext
321
+ ?? (existing || prov ? resolveUnknownRoutedContextWindow(contextCap) : undefined);
322
+ if (uncappedContext === undefined) return undefined;
323
+ // 真发现值才压低。resolveUnknownRoutedContextWindow 已经把 cap 当成窗口填进去了,不能再 min 一次。
324
+ const usedDiscoveredWindow = hintedContext !== undefined || knownMaxInput !== undefined || fallbackContext !== undefined;
325
+ const cappedContext = usedDiscoveredWindow
326
+ ? applyProviderContextCap(uncappedContext, contextCap)
327
+ : uncappedContext;
328
+ const contextWindow = cappedContext ?? uncappedContext;
329
+ const fallbackCapped = usedDiscoveredWindow
330
+ && contextCap !== undefined
331
+ && cappedContext !== undefined
332
+ && cappedContext !== uncappedContext;
333
+
334
+ const inputModalities = hinted.inputModalities
335
+ ?? base.inputModalities
336
+ ?? (fallback?.inputModalities ? [...fallback.inputModalities] : undefined)
337
+ ?? ["text"];
338
+ const reasoningEfforts = hinted.reasoningEfforts
339
+ ?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined)
340
+ ?? base.reasoningEfforts
341
+ ?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined);
342
+ const maxOutputTokens = positiveSafeInteger(hinted.maxOutputTokens, base.maxOutputTokens)
343
+ ?? (existing || prov ? positiveSafeInteger(fallback?.maxOutputTokens) : undefined);
344
+ // The model's own measured input ceiling still applies when discovery gave us nothing:
345
+ // GPT-5.6 advertises a 1.05M window but refuses input past 922k.
346
+ const effectiveMaxInput = knownMaxInput ?? fallbackMaxInput;
347
+ const maxInputTokens = effectiveMaxInput !== undefined
348
+ ? Math.min(effectiveMaxInput, contextWindow)
349
+ : contextWindow;
350
+ const softCandidates = [
351
+ hinted.autoCompactTokenLimit,
352
+ base.autoCompactTokenLimit,
353
+ fallback?.autoCompactTokenLimit,
354
+ configuredAutoCompactTokenLimit(prov, target.model),
355
+ ].filter((value): value is number => typeof value === "number" && value > 0);
356
+ // A generic 128k synthesis is a catalog compatibility fallback, not evidence
357
+ // that a configured soft policy has an authoritative window to clamp against.
358
+ const hasAuthoritativeAutoCompactBasis = hintedContext !== undefined
359
+ || fallbackContext !== undefined
360
+ || contextCap !== undefined;
361
+ const autoCompactTokenLimit = hasAuthoritativeAutoCompactBasis && softCandidates.length > 0
362
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, Math.min(...softCandidates))
363
+ : undefined;
364
+
365
+ return {
366
+ ...hinted,
367
+ inputModalities,
368
+ ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
369
+ contextWindow,
370
+ maxInputTokens,
371
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
372
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
373
+ ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
374
+ };
375
+ }
@@ -0,0 +1,229 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { effectiveProviderAlias } from "../../providers/default-aliases";
3
+ import { identifyRoutedModel } from "../../adapters/identity";
4
+ import { COMBO_NAMESPACE } from "../../combos";
5
+ import {
6
+ CODEX_CUSTOM_MODEL_CATALOG_KIND,
7
+ applyCatalogMetadata,
8
+ applyNativeOpenAiContextOverride,
9
+ applyRoutedCodexToolMode,
10
+ catalogModelSlug,
11
+ ensureStrictCatalogFields,
12
+ normalizeRoutedCatalogEntry,
13
+ normalizeServiceTiers,
14
+ } from "./parsing";
15
+ import type { CatalogModel, RawEntry } from "./parsing";
16
+ import {
17
+ hasNativeOpenAiCapabilityMetadata,
18
+ upstreamNativeEntry,
19
+ type NativeContextLimitsInput,
20
+ } from "./metadata";
21
+ import {
22
+ applyCatalogModelMetadata,
23
+ applyReasoningLevels,
24
+ ensureGpt56ReasoningLevels,
25
+ ensureUltraReasoningLevel,
26
+ isGpt56NativeSlug,
27
+ } from "./effort";
28
+ import { CATALOG_INACTIVE_REASON_FIELD, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
29
+
30
+ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
31
+ if (priority !== 9) clone.priority = priority;
32
+ applyNativeOpenAiContextOverride(clone, contextCap);
33
+ // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
34
+ // Older natives (gpt-5.5) get mock max + ultra
35
+ // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
36
+ if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
37
+ return ensureStrictCatalogFields(normalizeServiceTiers(clone));
38
+ }
39
+
40
+ export function isExactComboCatalogModel(
41
+ model: CatalogModel | undefined,
42
+ exactComboSlugs: ReadonlySet<string>,
43
+ ): boolean {
44
+ return model?.provider === COMBO_NAMESPACE && exactComboSlugs.has(catalogModelSlug(model));
45
+ }
46
+
47
+ export function isExactComboCatalogEntry(
48
+ entry: RawEntry,
49
+ exactComboSlugs: ReadonlySet<string>,
50
+ ): boolean {
51
+ return entry.owned_by === COMBO_NAMESPACE
52
+ && typeof entry.slug === "string"
53
+ && exactComboSlugs.has(entry.slug);
54
+ }
55
+
56
+ /**
57
+ * Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
58
+ * ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
59
+ * lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
60
+ * The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
61
+ * that is dropped for display. Google Antigravity is relabeled to the compact `agy/` prefix for
62
+ * the same reason: `google-antigravity/` alone consumes most of the picker row. That prefix comes
63
+ * from the row's own `providerAlias`, decided once per gather flight; `null` means a cross-provider
64
+ * collision suppressed it and the canonical slug stands. This is the raw-slug path only -- a
65
+ * configured `modelAliases` entry is labeled by the effective-alias path in
66
+ * catalog/provider-fetch.ts (#2960) and keeps the canonical provider name. All other providers
67
+ * keep the raw slug exactly as before.
68
+ */
69
+ function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<OcxConfig, "providers">): string {
70
+ const slash = slug.indexOf("/");
71
+ if (slash <= 0) return slug;
72
+ const provider = slug.slice(0, slash);
73
+ let modelId = slug.slice(slash + 1);
74
+ if (provider === "google-antigravity") {
75
+ if (model?.providerAlias === null) return slug;
76
+ const alias = (typeof model?.providerAlias === "string" && model.providerAlias.trim().length > 0)
77
+ ? model.providerAlias.trim()
78
+ : effectiveProviderAlias(provider, undefined, config);
79
+ return alias ? `${alias}/${modelId}` : slug;
80
+ }
81
+ if (provider === "command-code" || provider === "commandcode") {
82
+ const m = modelId.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
83
+ if (m && modelId.startsWith(`${m[1]}-${m[1]}-`)) modelId = modelId.slice(m[1]!.length + 1);
84
+ return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${modelId}`;
85
+ }
86
+ return slug;
87
+ }
88
+
89
+ function preservePinnedNativeCustomReasoning(model?: CatalogModel): boolean {
90
+ return model !== undefined
91
+ && model.catalogKind === CODEX_CUSTOM_MODEL_CATALOG_KIND
92
+ && hasNativeOpenAiCapabilityMetadata(model.id)
93
+ && Array.isArray(model.reasoningEfforts);
94
+ }
95
+
96
+ /**
97
+ * Cria uma entrada nativa ou roteada a partir do snapshot upstream, de um clone
98
+ * do template ou de campos mínimos. Aplica os metadados e limites pertinentes
99
+ * sem alterar o template nem herdar sua marca de nome ou histórico de prioridade.
100
+ */
101
+ export function deriveEntry(
102
+ template: RawEntry | null,
103
+ slug: string,
104
+ desc: string,
105
+ priority: number,
106
+ model?: CatalogModel,
107
+ exactComboSlugs: ReadonlySet<string> = new Set(),
108
+ contextCap?: NativeContextLimitsInput,
109
+ ): RawEntry {
110
+ const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
111
+ // Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
112
+ const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
113
+ const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
114
+ ? upstreamNativeEntry(model.id)
115
+ : null;
116
+ const isRouted = model !== undefined;
117
+ if (!isRouted && !slug.includes("/")) {
118
+ // Supported native slug covered by the upstream snapshot: use the REAL entry (exact
119
+ // reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
120
+ // instead of cloning an older template.
121
+ const upstream = upstreamNativeEntry(slug);
122
+ if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
123
+ }
124
+ if (template || codexForwardNativeCapabilityAlias) {
125
+ const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
126
+ delete e.opencodex_native_display_name;
127
+ // A cached template may carry display-order history; each new row owns its natural rank.
128
+ delete e[SPAWN_PRIORITY_FIELD];
129
+ e.slug = slug;
130
+ e.display_name = routedDisplayName(slug, model);
131
+ e.description = desc;
132
+ e.priority = priority;
133
+ e.visibility = "list";
134
+ if ("upgrade" in e) e.upgrade = null;
135
+ delete e.availability_nux; // don't replay another model's "now available" NUX
136
+ // Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
137
+ // and advertise the reasoning ladder Codex accepts.
138
+ if (isRouted) {
139
+ // A routed model is NOT the native template: never inherit its context
140
+ // window when /models omits context metadata (#992). Known metadata
141
+ // restores exact values below; an enabled Context cap fills the gap;
142
+ // otherwise the strict-fields fallback supplies the 128k triple.
143
+ if (!codexForwardNativeCapabilityAlias) {
144
+ delete e.context_window;
145
+ delete e.max_context_window;
146
+ delete e.auto_compact_token_limit;
147
+ }
148
+ // Native id for identity text + metadata lookups — the slug may be an encoded
149
+ // alias (`provider/vendor-model`); the model object carries the native id.
150
+ const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
151
+ if (typeof e.base_instructions === "string") {
152
+ // Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
153
+ // (leaking that into base_instructions is a non-first-party signature → ToS risk).
154
+ e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
155
+ }
156
+ applyReasoningLevels(
157
+ e,
158
+ model?.reasoningEfforts,
159
+ model?.defaultReasoningEffort,
160
+ preserveExactReasoning
161
+ || codexForwardNativeCapabilityAlias !== null
162
+ || preservePinnedNativeCustomReasoning(model),
163
+ );
164
+ // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
165
+ // native tool/search/responses-lite contract while preserving the routed slug and wire id.
166
+ if (!codexForwardNativeCapabilityAlias) {
167
+ normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true, model?.codexToolMode);
168
+ } else if (model?.codexToolMode !== undefined) {
169
+ applyRoutedCodexToolMode(e, model.codexToolMode);
170
+ }
171
+ if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
172
+ applyCatalogModelMetadata(e, model);
173
+ if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
174
+ // Additive only. `visibility` is untouched: an inactive row must still be OFFERED, which is
175
+ // the whole point of #1711 — operator disable is what removes rows, and it stays a separate
176
+ // path from this one.
177
+ if (model?.quotaInactiveReason) e[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
178
+ } else {
179
+ applyNativeOpenAiContextOverride(e, contextCap);
180
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
181
+ else ensureUltraReasoningLevel(e);
182
+ // Older natives do not support Responses Lite. A newer template must not enable
183
+ // reasoning.context or WebSockets on those models.
184
+ if (!isGpt56NativeSlug(slug)) {
185
+ delete e.use_responses_lite;
186
+ delete e.supports_websockets;
187
+ }
188
+ }
189
+ return ensureStrictCatalogFields(normalizeServiceTiers(e), {
190
+ preserveExactInputModalities: preserveExact,
191
+ isRouted,
192
+ });
193
+ }
194
+ // Fallback when no template is available (best-effort; strict parser may need more).
195
+ // Routed fallbacks default to code-mode tool exposure (or shell mode when codexToolMode === "shell");
196
+ // otherwise the nested catalog expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
197
+ // Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
198
+ const isCursorFallback = isRouted && model?.provider === "cursor";
199
+ const entry: RawEntry = {
200
+ slug, display_name: routedDisplayName(slug, model), description: desc,
201
+ shell_type: "unified_exec", visibility: "list", supported_in_api: true,
202
+ priority, base_instructions: "You are a helpful coding assistant.",
203
+ ...(isRouted
204
+ ? isCursorFallback
205
+ ? { supports_search_tool: true }
206
+ : { web_search_tool_type: "text_and_image", supports_search_tool: true }
207
+ : {}),
208
+ };
209
+ if (isRouted) {
210
+ applyRoutedCodexToolMode(entry, model?.codexToolMode);
211
+ applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning || preservePinnedNativeCustomReasoning(model));
212
+ }
213
+ else {
214
+ applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
215
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
216
+ }
217
+ if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
218
+ applyCatalogModelMetadata(entry, model);
219
+ if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
220
+ // Same additive stamp as the templated path above. A routed row that reaches the no-template
221
+ // fallback is still a served row, so omitting it here would make the field depend on whether a
222
+ // template happened to be cached — which is exactly what the regression test caught.
223
+ if (model?.quotaInactiveReason) entry[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
224
+ if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
225
+ return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
226
+ preserveExactInputModalities: preserveExact,
227
+ isRouted,
228
+ });
229
+ }
@@ -39,7 +39,6 @@ import { nativeOpenAiCapabilitySourceSlug, SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
39
39
  import { isReserveCatalogProjection } from "./reserve";
40
40
  import { loadBundledCodexCatalog } from "./bundled";
41
41
  import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
42
- import { deriveEntry } from "./sync";
43
42
  import {
44
43
  formatClampLogLines,
45
44
  formatRuntimeLogLine,