@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,902 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
4
+ import { delimiter, dirname, join, resolve } from "node:path";
5
+ import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
6
+ import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
7
+ import {
8
+ clearModelCache,
9
+ clearProviderDiscoveryStatus,
10
+ DEFAULT_MODEL_CACHE_TTL_MS,
11
+ getFreshCached,
12
+ getStaleCached,
13
+ isModelsFetchCoolingDown,
14
+ markModelsFetchFailure,
15
+ markProviderDiscoveryFailed,
16
+ markProviderDiscoveryOk,
17
+ shouldLogDiscoveryFailure,
18
+ setCached,
19
+ type ProviderModelDiscoveryFailure,
20
+ } from "../model-cache";
21
+ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
22
+ import type { OcxConfig, OcxProviderConfig } from "../../types";
23
+ import { modelInList } from "../../types";
24
+ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
25
+ import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
26
+ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
27
+ import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
28
+ import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
29
+ import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
30
+ import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
31
+ import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
32
+ import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
33
+ import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
34
+ import {
35
+ COMBO_NAMESPACE,
36
+ comboModelId,
37
+ getCombo,
38
+ listComboIds,
39
+ targetKey,
40
+ } from "../../combos";
41
+ import type { NormalizedComboConfig } from "../../combos/types";
42
+ import {
43
+ ProviderOutboundPolicyError,
44
+ providerOutboundGet,
45
+ providerRedirectError,
46
+ } from "../../lib/provider-outbound";
47
+ import { redactSecretString } from "../../lib/redact";
48
+ import {
49
+ extractProviderModelItems,
50
+ readBoundedDiscoveryJson,
51
+ resolveProviderModelDiscovery,
52
+ type ModelDiscoveryResponseFailure,
53
+ type ProviderModelsApiItem,
54
+ } from "../../providers/model-discovery";
55
+ import upstreamModelsSnapshot from "../data/upstream-models.json";
56
+ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
57
+
58
+
59
+ import { JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
60
+ import type { CatalogModel } from "./parsing";
61
+ import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
62
+ import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
63
+ import type { ComboCatalogOmission } from "./aggregation";
64
+
65
+ /** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
66
+ * Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
67
+ interface GatherFlightResult {
68
+ models: CatalogModel[];
69
+ comboOmissions: ComboCatalogOmission[];
70
+ }
71
+
72
+ const gatherInflight = new Map<string, Promise<GatherFlightResult>>();
73
+ const MAX_CONCURRENT_CATALOG_GATHERS = 8;
74
+ const gatherGate = createAdmissionGate("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
75
+
76
+ export class CatalogGatherBusyError extends ResourceAdmissionError {
77
+ override readonly code = "catalog_busy";
78
+ readonly retryAfterSeconds = 1;
79
+ constructor() {
80
+ super("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
81
+ this.name = "CatalogGatherBusyError";
82
+ }
83
+ }
84
+
85
+ export function catalogGatherAdmissionMetrics(): AdmissionMetrics {
86
+ return gatherGate.metrics();
87
+ }
88
+
89
+ function stableJson(value: unknown): string {
90
+ return JSON.stringify(value, (_key, nested) => {
91
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
92
+ return Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)));
93
+ }
94
+ return nested;
95
+ });
96
+ }
97
+
98
+ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record<string, unknown> {
99
+ return {
100
+ n: name,
101
+ // Preserve the persisted tri-state. Registry enrichment may turn an omitted value into
102
+ // `false` while an explicit `true` stays live, so those callers must not share a flight.
103
+ live: prov.liveModels ?? null,
104
+ base: prov.baseUrl ?? "",
105
+ adapter: prov.adapter ?? "",
106
+ models: [...(prov.models ?? [])].sort(),
107
+ selected: [...(prov.selectedModels ?? [])].sort(),
108
+ defaultModel: prov.defaultModel ?? null,
109
+ ctx: prov.contextWindow ?? null,
110
+ ctxW: prov.modelContextWindows ?? null,
111
+ maxIn: prov.modelMaxInputTokens ?? null,
112
+ inMod: prov.modelInputModalities ?? null,
113
+ re: prov.modelReasoningEfforts ?? null,
114
+ defRe: prov.modelDefaultReasoningEfforts ?? null,
115
+ rsSum: prov.modelSupportsReasoningSummaries ?? null,
116
+ rsDel: prov.modelReasoningSummaryDelivery ?? null,
117
+ noVis: [...(prov.noVisionModels ?? [])].sort(),
118
+ ptc: prov.parallelToolCalls ?? null,
119
+ gMode: prov.googleMode ?? null,
120
+ };
121
+ }
122
+
123
+ function gatherFlightKey(config: OcxConfig): string {
124
+ const providers = Object.entries(config.providers)
125
+ .filter(([, prov]) => prov.disabled !== true)
126
+ .map(([name, prov]) => providerCatalogFingerprint(name, prov))
127
+ .sort((a, b) => String(a.n).localeCompare(String(b.n)));
128
+ const assembly = stableJson({
129
+ providers,
130
+ disabledModels: [...(config.disabledModels ?? [])].sort(),
131
+ combos: config.combos ?? {},
132
+ customModels: (config.customModels ?? []).map((cm) => ({
133
+ p: cm.provider,
134
+ m: cm.modelId,
135
+ d: cm.displayName ?? null,
136
+ cw: cm.contextWindow ?? null,
137
+ im: cm.inputModalities ?? null,
138
+ })),
139
+ caps: config.providerContextCaps ?? null,
140
+ });
141
+ const digest = createHash("sha256").update(assembly).digest("hex").slice(0, 16);
142
+ return `${digest}#${config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS}`;
143
+ }
144
+
145
+ /** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */
146
+ export function clearGatherRoutedModelsInflight(): void {
147
+ gatherInflight.clear();
148
+ }
149
+
150
+ export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
151
+ const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
152
+ return typeof configured === "number" && configured > 0 ? configured : undefined;
153
+ }
154
+
155
+ export function configuredInputModalities(prov: OcxProviderConfig, id: string): string[] | undefined {
156
+ const modalities = modelRecordValue(prov.modelInputModalities, id);
157
+ return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
158
+ }
159
+
160
+ export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined {
161
+ const configured = modelRecordValue(prov.modelMaxInputTokens, id);
162
+ return typeof configured === "number" && configured > 0 ? configured : undefined;
163
+ }
164
+
165
+ function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
166
+ if (!prov) return undefined;
167
+ const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
168
+ if (explicit !== undefined) return explicit;
169
+ return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
170
+ }
171
+
172
+ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
173
+ void name;
174
+ const configuredCap = configuredContextWindow(prov, model.id);
175
+ const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
176
+ let inputModalities = configuredInputModalities(prov, model.id);
177
+ // Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
178
+ // (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
179
+ // gates attachments client-side on input_modalities, and a text-only entry would block images
180
+ // before the sidecar ever runs ("This model does not support image inputs").
181
+ if (modelInList(prov.noVisionModels, model.id)) {
182
+ const base = inputModalities ?? model.inputModalities ?? ["text"];
183
+ inputModalities = base.includes("image") ? [...base] : [...base, "image"];
184
+ }
185
+ const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
186
+ const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
187
+ const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
188
+ const hinted = {
189
+ ...model,
190
+ ...(configuredCap !== undefined
191
+ ? {
192
+ contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
193
+ ? Math.min(model.contextWindow, configuredCap)
194
+ : configuredCap,
195
+ }
196
+ : {}),
197
+ ...(inputModalities ? { inputModalities } : {}),
198
+ ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
199
+ ...(configuredMaxInput !== undefined
200
+ ? {
201
+ maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
202
+ ? Math.min(model.maxInputTokens, configuredMaxInput)
203
+ : configuredMaxInput,
204
+ }
205
+ : {}),
206
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
207
+ ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
208
+ ...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
209
+ // Default-on for openai-chat providers (explicit false opts out); other adapters
210
+ // advertise only on explicit opt-in.
211
+ ...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
212
+ ? { parallelToolCalls: true }
213
+ : {}),
214
+ };
215
+ const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
216
+ if (providerCap !== undefined && capped !== hinted.contextWindow) {
217
+ return { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true };
218
+ }
219
+ return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
220
+ }
221
+
222
+ export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
223
+ const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap);
224
+ const { provider: _provider, id: _id, ...hints } = hinted;
225
+ return hints;
226
+ }
227
+
228
+ export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[], contextCap?: number): CatalogModel[] {
229
+ return models.map(model => applyProviderConfigHints(name, prov, model, contextCap));
230
+ }
231
+
232
+ export function isDatedVariantId(liveId: string, configuredId: string): boolean {
233
+ if (!liveId.startsWith(`${configuredId}-`)) return false;
234
+ return /^\d{8}$/.test(liveId.slice(configuredId.length + 1));
235
+ }
236
+
237
+ export const lastDropWarnSignature = new Map<string, string>();
238
+ let lastWarningReconciledGeneration = 0;
239
+
240
+ export function reconcileProviderFetchWarnings(generation: number): number {
241
+ if (generation <= lastWarningReconciledGeneration) return 0;
242
+ const removed = lastDropWarnSignature.size;
243
+ lastDropWarnSignature.clear();
244
+ lastWarningReconciledGeneration = generation;
245
+ return removed;
246
+ }
247
+
248
+ export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
249
+
250
+ export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
251
+ kimi: new Set([
252
+ "k3[1m]",
253
+ "kimi-k2.7-code",
254
+ "kimi-k2.7-code-highspeed",
255
+ "kimi-k2.6",
256
+ "kimi-k2.5",
257
+ ]),
258
+ xai: new Set([
259
+ "grok-4.3",
260
+ "grok-4.20-0309-reasoning",
261
+ "grok-4.20-0309-non-reasoning",
262
+ "grok-build-0.1",
263
+ "grok-composer-2.5-fast",
264
+ ]),
265
+ };
266
+
267
+ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds: string[]): void {
268
+ const signature = [...droppedConfiguredIds].sort().join(",");
269
+ if (lastDropWarnSignature.get(name) === signature) return;
270
+ lastDropWarnSignature.set(name, signature);
271
+ console.warn(
272
+ `[opencodex] Provider model discovery for "${name}" omitted configured model ids; dropping them from the authoritative live catalog: ${droppedConfiguredIds.join(", ")}.`,
273
+ );
274
+ }
275
+
276
+ export function isGlm52ModelId(id: string): boolean {
277
+ const normalized = id.toLowerCase();
278
+ return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
279
+ }
280
+
281
+ function plainRecord(value: unknown): Record<string, unknown> | undefined {
282
+ return value !== null && typeof value === "object" && !Array.isArray(value)
283
+ ? value as Record<string, unknown>
284
+ : undefined;
285
+ }
286
+
287
+ const MODEL_DISCOVERY_METADATA_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
288
+
289
+ function positiveSafeInteger(...values: unknown[]): number | undefined {
290
+ return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined;
291
+ }
292
+
293
+ function normalizedMetadataString(raw: string, maxLength: number): string | undefined {
294
+ if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined;
295
+ const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength);
296
+ return normalized || undefined;
297
+ }
298
+
299
+ function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): string[] | undefined {
300
+ if (!Array.isArray(value)) return undefined;
301
+ const out: string[] = [];
302
+ const maxInspectedItems = Math.max(maxItems * 8, maxItems);
303
+ for (let i = 0; i < value.length && i < maxInspectedItems; i += 1) {
304
+ const raw = value[i];
305
+ if (typeof raw !== "string") continue;
306
+ const normalized = normalizedMetadataString(raw, maxLength);
307
+ if (normalized && !out.includes(normalized)) out.push(normalized);
308
+ if (out.length >= maxItems) break;
309
+ }
310
+ return out.length > 0 ? out : undefined;
311
+ }
312
+
313
+ function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
314
+ const metadata = plainRecord(item.metadata);
315
+ const metadataCapabilities = metadata?.capabilities;
316
+ const capabilityRecord = plainRecord(metadataCapabilities) ?? plainRecord(item.capabilities);
317
+ const out = new Set<string>();
318
+ for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
319
+ for (const capability of normalizedStringList(list) ?? []) out.add(capability);
320
+ }
321
+ const capabilityFields = capabilityRecord ?? {};
322
+ let inspectedCapabilityFields = 0;
323
+ for (const key in capabilityFields) {
324
+ if (!Object.hasOwn(capabilityFields, key)) continue;
325
+ inspectedCapabilityFields += 1;
326
+ if (inspectedCapabilityFields > 256 || out.size >= 32) break;
327
+ if (capabilityFields[key] === true) {
328
+ const normalized = normalizedMetadataString(key, 64);
329
+ if (normalized) out.add(normalized);
330
+ }
331
+ }
332
+ for (const field of ["supports_tools", "supports_tool_calling", "supports_function_calling"] as const) {
333
+ if (item[field] === true) out.add("tools");
334
+ }
335
+ for (const field of ["supports_reasoning", "reasoning"] as const) {
336
+ if (item[field] === true) out.add("reasoning");
337
+ }
338
+ return out.size > 0 ? [...out].filter(Boolean).slice(0, 32) : undefined;
339
+ }
340
+
341
+ function modelInputModalities(
342
+ item: ProviderModelsApiItem,
343
+ capabilities: readonly string[] | undefined,
344
+ ): string[] | undefined {
345
+ const metadata = plainRecord(item.metadata);
346
+ const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
347
+ const explicit = normalizedStringList(
348
+ item.input_modalities
349
+ ?? item.modalities
350
+ ?? metadata?.input_modalities
351
+ ?? capabilityRecord?.input_modalities,
352
+ 8,
353
+ 24,
354
+ )?.filter(value => (
355
+ // Codex parses `input_modalities` as a closed enum of text | image | audio. A provider that
356
+ // advertises anything else (zenmux reports "video") must not reach the catalog: Codex rejects
357
+ // the whole file, so plugins, apps and MCP servers all stop loading over one model's metadata.
358
+ value === "text" || value === "image" || value === "audio"
359
+ ));
360
+ if (explicit && explicit.length > 0) return explicit;
361
+ if (capabilityRecord?.vision === false) return ["text"];
362
+ if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
363
+ return ["text", "image"];
364
+ }
365
+ return undefined;
366
+ }
367
+
368
+ export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
369
+ const metadata = plainRecord(item.metadata);
370
+ const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
371
+ const limits = plainRecord(metadata?.limits);
372
+ const contextWindow =
373
+ positiveSafeInteger(
374
+ limits?.max_context_length,
375
+ metadata?.context_length,
376
+ item.context_length,
377
+ item.context_size,
378
+ item.max_model_len,
379
+ item.max_context_length,
380
+ );
381
+ const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
382
+ const rawReasoningEfforts = capabilityRecord?.reasoning_effort ?? item.reasoning_efforts;
383
+ const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24);
384
+ const reasoningEfforts = listedReasoningEfforts
385
+ ? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
386
+ : typeof rawReasoningEfforts === "boolean"
387
+ ? (rawReasoningEfforts
388
+ ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
389
+ ? ["low", "medium", "high", "xhigh", "max"]
390
+ : ["low", "medium", "high", "xhigh"])
391
+ : [])
392
+ : undefined;
393
+ const capabilities = modelCapabilities(item);
394
+ const inputModalities = modelInputModalities(item, capabilities);
395
+ return {
396
+ ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
397
+ ...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
398
+ ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
399
+ ...(inputModalities ? { inputModalities } : {}),
400
+ ...(capabilities ? { capabilities } : {}),
401
+ };
402
+ }
403
+
404
+ function boundedOwnedBy(value: unknown): string | undefined {
405
+ if (typeof value !== "string" || value.length === 0 || value.length > 256) return undefined;
406
+ if (MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)) return undefined;
407
+ return value;
408
+ }
409
+
410
+ export async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
411
+ if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
412
+ const seedVertexDefault = prov.adapter === "google"
413
+ && prov.googleMode === "vertex"
414
+ && (prov.models?.length ?? 0) === 0
415
+ && Boolean(prov.defaultModel);
416
+ const configuredIds = seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : (prov.models ?? []);
417
+ const configured: CatalogModel[] = configuredIds.map(id => ({
418
+ id,
419
+ provider: name,
420
+ ...catalogHintsFromProviderConfig(name, prov, id, contextCap),
421
+ }));
422
+ // Static catalogs never need an OAuth refresh or an upstream model request. Clear any
423
+ // discovery failure left by an older live configuration even when the account is logged out.
424
+ if (prov.liveModels === false) {
425
+ clearProviderDiscoveryStatus(name);
426
+ return configured;
427
+ }
428
+ const apiKey = await resolveModelsAuthToken(name, prov);
429
+ // A configured default is a real callable selector and must remain discoverable when a
430
+ // compatible provider's live /models request fails (issue #308). Keep this separate from the
431
+ // explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero
432
+ // rows, while a failed live discovery may degrade to the default selector.
433
+ const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic"
434
+ ? configured
435
+ : [{
436
+ id: prov.defaultModel,
437
+ provider: name,
438
+ ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap),
439
+ }];
440
+ const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
441
+ const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
442
+ vertexDefaultSeed && !models.some(model => model.id === vertexDefaultSeed.id)
443
+ ? [...models, vertexDefaultSeed]
444
+ : models
445
+ );
446
+ if (prov.adapter === "cursor") {
447
+ if (!apiKey) return configured;
448
+ // Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
449
+ // variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
450
+ // suffix) but filter the static seed to the bases the account actually has — so models not on the
451
+ // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
452
+ const cachedCursor = getFreshCached(name, ttlMs);
453
+ if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
454
+ if (isModelsFetchCoolingDown(name)) {
455
+ const cooling = getStaleCached(name);
456
+ return cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured;
457
+ }
458
+ const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
459
+ if (liveResult.ok) {
460
+ const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
461
+ const result = available.length > 0 ? available : configured;
462
+ // Count what discovery actually returned, not the configured rows we fall back to.
463
+ markProviderDiscoveryOk(name, liveResult.models.length);
464
+ setCached(name, result);
465
+ return result;
466
+ }
467
+ markModelsFetchFailure(name);
468
+ markProviderDiscoveryFailed(name, { reason: "provider" });
469
+ console.warn(
470
+ `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
471
+ );
472
+ const staleCursor = getStaleCached(name);
473
+ return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured;
474
+ }
475
+ if (prov.authMode === "oauth" && !apiKey) {
476
+ // No usable token (logged out, or account marked needsReauth). Still surface the
477
+ // configured static catalog so the GUI Models tab / rail counts are not empty —
478
+ // matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
479
+ return configured;
480
+ }
481
+ const fresh = getFreshCached(name, ttlMs);
482
+ if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
483
+ if (isModelsFetchCoolingDown(name)) {
484
+ // A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
485
+ // fetch timeout on every catalog poll — the dashboard polls this path per page load.
486
+ const stale = getStaleCached(name);
487
+ return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : failedDiscoveryConfigured;
488
+ }
489
+ const discovery = resolveProviderModelDiscovery(name, prov);
490
+ const { url, headers } = buildModelsRequest(prov, apiKey, name);
491
+ const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
492
+ ? "vertex-aiplatform"
493
+ : "provider-models";
494
+ const failedDiscoveryFallback = (
495
+ failure: ProviderModelDiscoveryFailure,
496
+ ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
497
+ // Decide logging BEFORE recording the new status, so we can compare against the prior one and
498
+ // suppress an identical repeated failure (#395 log flood). The failure stays observable via the
499
+ // discovery-status API regardless.
500
+ const shouldLog = shouldLogDiscoveryFailure(name, failure);
501
+ markModelsFetchFailure(name);
502
+ markProviderDiscoveryFailed(name, failure);
503
+ const stale = getStaleCached(name);
504
+ return {
505
+ models: stale
506
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
507
+ : failedDiscoveryConfigured,
508
+ fallback: stale ? "stale" : "configured",
509
+ shouldLog,
510
+ };
511
+ };
512
+ try {
513
+ const res = await providerOutboundGet(name, prov, url, {
514
+ headers,
515
+ signal: AbortSignal.timeout(8000),
516
+ });
517
+ const redirectError = await providerRedirectError(res, url);
518
+ if (redirectError) {
519
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
520
+ if (shouldLog) {
521
+ console.warn(
522
+ `[opencodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`,
523
+ );
524
+ }
525
+ return models;
526
+ }
527
+ if (!res.ok) {
528
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
529
+ if (shouldLog) {
530
+ console.warn(
531
+ `[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
532
+ );
533
+ }
534
+ return models;
535
+ }
536
+
537
+ const contentType = (
538
+ res.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "missing"
539
+ ).slice(0, 80);
540
+ const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
541
+ if (!bounded.ok) {
542
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
543
+ const diagnostic = bounded.reason === "response_too_large"
544
+ ? `exceeded the ${discovery.maxResponseBytes}-byte response limit`
545
+ : contentType === "application/json" || contentType.endsWith("+json")
546
+ ? "returned invalid JSON in a 2xx response"
547
+ : "returned a non-JSON 2xx response";
548
+ if (shouldLog) {
549
+ console.warn(
550
+ `[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
551
+ );
552
+ }
553
+ return models;
554
+ }
555
+ const extracted = extractProviderModelItems(bounded.value, discovery);
556
+ if (!extracted.ok) {
557
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
558
+ const diagnostic: Record<ModelDiscoveryResponseFailure, string> = {
559
+ response_too_large: "returned an oversized 2xx response",
560
+ invalid_json: "returned invalid JSON in a 2xx response",
561
+ invalid_shape: "returned malformed 2xx data",
562
+ too_many_models: `exceeded the ${discovery.maxModels}-row model limit`,
563
+ };
564
+ if (shouldLog) {
565
+ console.warn(
566
+ `[opencodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
567
+ );
568
+ }
569
+ return models;
570
+ }
571
+ const items = extracted.items;
572
+ const live = items.map(m => {
573
+ const ownedBy = boundedOwnedBy(m.owned_by);
574
+ return applyProviderConfigHints(name, prov, {
575
+ id: m.id,
576
+ provider: name,
577
+ ...(ownedBy ? { owned_by: ownedBy } : {}),
578
+ ...catalogHintsFromModelsApiItem(name, m),
579
+ }, contextCap);
580
+ })
581
+ .filter(m => shouldExposeProviderModel(name, m.id));
582
+ // Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
583
+ // `live`; otherwise configured entries would be reported as discovered ones.
584
+ const liveModelCount = live.length;
585
+ const liveIds = new Set(live.map(m => m.id));
586
+ // Dated-release aliases (Anthropic pattern): older models may appear in the live catalog
587
+ // ONLY under their dated id (claude-haiku-4-5-20251001) while the config names the
588
+ // API-valid alias (claude-haiku-4-5). Such aliases are real, callable models — keep them
589
+ // in the authoritative catalog (alias id, hints from the dated live entry) instead of
590
+ // dropping them and warning on every poll.
591
+ const droppedConfiguredIds: string[] = [];
592
+ for (const m of configured) {
593
+ if (liveIds.has(m.id)) continue;
594
+ const dated = live.find(l => isDatedVariantId(l.id, m.id));
595
+ if (dated) {
596
+ // Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
597
+ live.push(applyProviderConfigHints(name, prov, { ...dated, id: m.id }, contextCap));
598
+ } else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
599
+ live.push(m);
600
+ } else {
601
+ droppedConfiguredIds.push(m.id);
602
+ }
603
+ }
604
+ if (live.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
605
+ console.warn(
606
+ `[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"}.`,
607
+ );
608
+ } else if (droppedConfiguredIds.length > 0
609
+ && name !== OPENAI_API_PROVIDER_ID
610
+ && !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
611
+ warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
612
+ }
613
+ markProviderDiscoveryOk(name, liveModelCount);
614
+ setCached(name, live);
615
+ return live;
616
+ } catch (error) {
617
+ if (error instanceof ProviderOutboundPolicyError) {
618
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
619
+ if (shouldLog) {
620
+ console.warn(
621
+ `[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`,
622
+ );
623
+ }
624
+ return models;
625
+ }
626
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" });
627
+ if (shouldLog) {
628
+ console.warn(
629
+ `[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
630
+ );
631
+ }
632
+ return models;
633
+ }
634
+ }
635
+
636
+ export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
637
+ if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
638
+ return true;
639
+ }
640
+
641
+ export function shouldRetainConfiguredProviderModel(providerName: string, modelId: string): boolean {
642
+ if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
643
+ if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
644
+ return false;
645
+ }
646
+
647
+ export function filterCatalogVisibleModels(
648
+ models: CatalogModel[],
649
+ config: Pick<OcxConfig, "disabledModels" | "providers">,
650
+ ): CatalogModel[] {
651
+ const disabled = new Set(config.disabledModels ?? []);
652
+ const allowByProvider = new Map<string, Set<string>>();
653
+ for (const [name, prov] of Object.entries(config.providers)) {
654
+ const sel = prov.selectedModels;
655
+ if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
656
+ }
657
+ return models.filter(m => {
658
+ // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
659
+ for (const stored of disabled) {
660
+ // Combo management stores the public alias, while canonical `combo/<id>` references
661
+ // remain valid for backward compatibility through slugEquals below.
662
+ if (m.alias !== undefined && stored === catalogModelSlug(m)) return false;
663
+ if (slugEquals(stored, m.provider, m.id)) return false;
664
+ }
665
+ const allow = allowByProvider.get(m.provider);
666
+ return !allow || allow.has(m.id);
667
+ });
668
+ }
669
+
670
+ export async function gatherRoutedModels(
671
+ config: OcxConfig,
672
+ options?: { comboOmissions?: ComboCatalogOmission[] },
673
+ ): Promise<CatalogModel[]> {
674
+ const key = gatherFlightKey(config);
675
+ let promise = gatherInflight.get(key);
676
+ if (!promise) {
677
+ const lease = gatherGate.tryAcquire();
678
+ if (!lease) throw new CatalogGatherBusyError();
679
+ // Claim the slot synchronously before any await so same-key callers join this flight.
680
+ // Distinct keys keep their own entries — a second config must not evict the first.
681
+ const flight = gatherRoutedModelsUncached(config).finally(() => {
682
+ if (gatherInflight.get(key) === flight) gatherInflight.delete(key);
683
+ lease.release();
684
+ });
685
+ gatherInflight.set(key, flight);
686
+ promise = flight;
687
+ }
688
+ const { models, comboOmissions } = await promise;
689
+ if (options?.comboOmissions) {
690
+ options.comboOmissions.length = 0;
691
+ options.comboOmissions.push(...comboOmissions);
692
+ }
693
+ return models;
694
+ }
695
+
696
+ async function gatherRoutedModelsUncached(
697
+ config: OcxConfig,
698
+ ): Promise<GatherFlightResult> {
699
+ // Flight-local list: joiners copy from the resolved promise, not a process-global last write.
700
+ const localOmissions: ComboCatalogOmission[] = [];
701
+ const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
702
+ // Persisted provider entries can predate newer registry fields (noVisionModels,
703
+ // modelInputModalities, ...). The ROUTER merges registry seeds at request time
704
+ // (routedProviderConfig), so the proxy behaves correctly — the catalog listing must see the
705
+ // same merged view or its advertisements drift from actual proxy behavior (e.g. a
706
+ // vision-sidecar model advertised text-only, blocking image attachments app-side).
707
+ // Enrich a CLONE: hydrated defaults must never leak into the persisted config.
708
+ const activeProviders = Object.entries(config.providers)
709
+ .filter(([, prov]) => prov.disabled !== true)
710
+ .map(([name, prov]): [string, OcxProviderConfig] => {
711
+ const enriched = { ...prov };
712
+ enrichProviderFromRegistry(name, enriched);
713
+ return [name, enriched];
714
+ });
715
+ const lists = await Promise.all(
716
+ activeProviders.map(([name, prov]) => fetchProviderModels(name, prov, ttlMs, providerContextCap(config, name))),
717
+ );
718
+ const apiAugmented = augmentRoutedModelsWithRegistryOpenAiApiRows(lists.flat(), config);
719
+ const all = augmentRoutedModelsWithJawcodeMetadata(apiAugmented, activeProviders.map(([name]) => name), config.providers, config)
720
+ // Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
721
+ // intentionally mirrors Cursor's public model table, including Gemini image preview, so the
722
+ // exposure decision goes through shouldExposeRoutedModel (single choke point).
723
+ .filter(shouldExposeRoutedModel);
724
+ const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model]));
725
+ // [Decision Log]
726
+ // - 목적과 의도: 콤보 타겟에 native OpenAI(Codex login) 모델이 포함될 때 카탈로그에서
727
+ // 누락되는 버그(issue #268)를 수정. "openai" provider는 forward-auth(Codex login
728
+ // passthrough)이므로 fetchProviderModels가 항상 []를 반환하고, native slugs는
729
+ // 별도 정적 경로(nativeOpenAiSlugs)로만 노출됨. 따라서 memberByKey에
730
+ // openai/<slug> 키가 존재하지 않아 콤보가 조용히 drop됨.
731
+ // - 기존 구현 및 제약 조건: memberByKey는 routed provider /models fetch 결과로만 구성.
732
+ // - 검토한 주요 대안: (A) native slugs를 all 배열에 직접 push — /v1/models와 온디스크
733
+ // 카탈로그에서 native 모델이 중복 노출되는 부작용 발생. (B) memberByKey에만 synthetic
734
+ // CatalogModel을 주입 — 콤보 멤버 해석에만 사용하고 all에는 추가하지 않으므로 기존
735
+ // 노출 경로에 영향 없음.
736
+ // - 선택한 방식: (B) — synthetic entries를 memberByKey에만 주입.
737
+ // - 다른 대안 대신 이 방식을 선택한 이유: 기존 native 모델 노출 경로(/v1/models, 온디스크
738
+ // 카탈로그 sync, management API)를 전혀 변경하지 않고 콤보 resolution만 수선하기 때문.
739
+ // - 장점, 단점 및 영향: 장점 — 최소 수정, 기존 경로 무변경. 단점 — synthetic entries의
740
+ // capability 데이터가 static/upstream snapshot 기반이므로, 사용자가 커스텀 config
741
+ // 힌트(modelContextWindows 등)로 native 모델의 context window를 오버라이드한 경우
742
+ // 반영되지 않음. 하지만 nativeOpenAiContextWindow가 이미 config 오버라이드를
743
+ // 우선시하므로 실제 충돌 가능성은 낮음.
744
+ if (!hasComboTargets(config)) {
745
+ // Skip the native slug injection entirely when no combos are configured — avoids
746
+ // calling nativeOpenAiSlugs() (which reads the live Codex catalog from disk) for
747
+ // configs that will never need it.
748
+ } else {
749
+ const disabled = disabledNativeSlugs(config);
750
+ for (const slug of nativeOpenAiSlugs()) {
751
+ if (disabled.has(slug)) continue;
752
+ const contextWindow = nativeOpenAiContextWindow(slug);
753
+ if (contextWindow === undefined) continue;
754
+ const synthetic: CatalogModel = {
755
+ provider: "openai",
756
+ id: slug,
757
+ owned_by: "openai",
758
+ contextWindow,
759
+ maxInputTokens: contextWindow,
760
+ inputModalities: nativeInputModalities(slug),
761
+ reasoningEfforts: nativeReasoningEfforts(slug),
762
+ ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
763
+ };
764
+ const key = `openai/${slug}`;
765
+ // Only inject when not already present from a routed provider (an API-key
766
+ // "openai" provider could shadow the native one).
767
+ if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
768
+ }
769
+ }
770
+ for (const id of listComboIds(config)) {
771
+ const combo = getCombo(config, id);
772
+ if (!combo) continue;
773
+ const members = combo.targets
774
+ .map(target => memberByKey.get(targetKey(target)))
775
+ .filter((member): member is CatalogModel => member !== undefined);
776
+ const derived = deriveComboCatalogModel(id, combo, members);
777
+ if (derived) all.push(derived);
778
+ else warnUncataloguedComboOnce(id, combo, members, localOmissions);
779
+ }
780
+ replaceLastComboCatalogOmissions(localOmissions);
781
+ all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
782
+ // Enriched (registry-hydrated) provider clones, keyed by name — the same view used above so
783
+ // custom rows get the same noVisionModels / inputModalities treatment as discovered rows.
784
+ const enrichedByName = new Map(activeProviders);
785
+ const customModels = (config.customModels ?? []).map(cm => {
786
+ const rawProvider = config.providers[cm.provider];
787
+ const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
788
+ const base: CatalogModel = {
789
+ id: cm.modelId,
790
+ provider: cm.provider,
791
+ // Display-only label: never feeds routing (customModels are keyed by routedSlug below).
792
+ ...(cm.displayName ? { displayName: cm.displayName } : {}),
793
+ ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
794
+ ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
795
+ ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
796
+ };
797
+ // Vision-sidecar coverage ONLY: if the custom model is in the enriched provider's
798
+ // noVisionModels, advertise image input so the Codex app lets images reach the sidecar
799
+ // (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
800
+ // user override, so their explicit contextWindow / inputModalities / reasoning fields must be
801
+ // preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
802
+ const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
803
+ if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, base.id)) {
804
+ const current = base.inputModalities ?? ["text"];
805
+ if (!current.includes("image")) {
806
+ return { ...base, inputModalities: [...current, "image"] };
807
+ }
808
+ }
809
+ return base;
810
+ });
811
+ // Custom rows override discovered rows that encode to the same Codex-facing slug.
812
+ const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
813
+ const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
814
+ return { models: [...deduped, ...customModels], comboOmissions: localOmissions };
815
+ }
816
+
817
+ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
818
+ models: CatalogModel[],
819
+ config: OcxConfig,
820
+ ): CatalogModel[] {
821
+ const configured = config.providers[OPENAI_API_PROVIDER_ID];
822
+ if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models;
823
+ const entry = getProviderRegistryEntry(OPENAI_API_PROVIDER_ID);
824
+ if (!entry?.models) return models;
825
+
826
+ const existingById = new Map(
827
+ models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]),
828
+ );
829
+ const trustedRows = entry.models.map((id): CatalogModel => {
830
+ const officialContext = entry.modelContextWindows?.[id];
831
+ const officialMaxInput = entry.modelMaxInputTokens?.[id];
832
+ const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow;
833
+ const userMaxInput = configured.modelMaxInputTokens?.[id];
834
+ const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID);
835
+ const contextWindow = typeof officialContext === "number"
836
+ ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
837
+ : undefined;
838
+ const maxInputTokens = typeof officialMaxInput === "number"
839
+ ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
840
+ : undefined;
841
+ return {
842
+ provider: OPENAI_API_PROVIDER_ID,
843
+ id,
844
+ owned_by: OPENAI_API_PROVIDER_ID,
845
+ ...(contextWindow ? { contextWindow } : {}),
846
+ ...(maxInputTokens ? { maxInputTokens } : {}),
847
+ ...(entry.modelInputModalities?.[id] ? { inputModalities: [...entry.modelInputModalities[id]!] } : {}),
848
+ ...(entry.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...entry.modelReasoningEfforts[id]!] } : {}),
849
+ };
850
+ });
851
+
852
+ for (const trusted of trustedRows) {
853
+ const live = existingById.get(trusted.id);
854
+ if (!live) continue;
855
+ const liveSignature = normalizedOpenAiApiSignature(live);
856
+ const trustedSignature = normalizedOpenAiApiSignature(trusted);
857
+ if (liveSignature === trustedSignature) continue;
858
+ const warningKey = `${trusted.provider}/${trusted.id}\n${liveSignature}\n${trustedSignature}`;
859
+ if (openAiApiCollisionWarnings.has(warningKey)) continue;
860
+ openAiApiCollisionWarnings.add(warningKey);
861
+ console.warn(`[opencodex] replacing conflicting live OpenAI API metadata for ${trusted.provider}/${trusted.id} with trusted registry metadata`);
862
+ }
863
+
864
+ return [
865
+ ...models.filter(model => model.provider !== OPENAI_API_PROVIDER_ID),
866
+ ...trustedRows,
867
+ ];
868
+ }
869
+
870
+ export function augmentRoutedModelsWithJawcodeMetadata(
871
+ models: CatalogModel[],
872
+ providerNames: string[],
873
+ providers?: Record<string, OcxProviderConfig>,
874
+ caps?: Pick<OcxConfig, "providerContextCaps">,
875
+ ): CatalogModel[] {
876
+ const out = [...models];
877
+ const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
878
+ for (const provider of providerNames) {
879
+ if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
880
+ if (providers?.[provider]?.liveModels === false) continue;
881
+ const jawcodeProvider = resolveJawcodeProvider(provider);
882
+ if (!jawcodeProvider) continue;
883
+ for (const meta of listJawcodeModelMetadata(jawcodeProvider)) {
884
+ const key = `${provider}/${meta.id}`;
885
+ if (seen.has(key)) continue;
886
+ seen.add(key);
887
+ const contextCap = caps ? providerContextCap(caps, provider) : undefined;
888
+ const model: CatalogModel = {
889
+ provider,
890
+ id: meta.id,
891
+ owned_by: provider,
892
+ ...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}),
893
+ ...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
894
+ };
895
+ out.push({
896
+ ...model,
897
+ ...(providers?.[provider] ? applyProviderConfigHints(provider, providers[provider], model, contextCap) : {}),
898
+ });
899
+ }
900
+ }
901
+ return out;
902
+ }