@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -5,7 +5,11 @@ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
5
5
  import type { CodexAuthPolicyConfig } from "../codex/auth-context";
6
6
  import { isCodexReserveRequestEligible } from "../codex/loopback-target";
7
7
  import type { DataPlaneAdmission } from "../server/auth-cors";
8
- import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility";
8
+ import {
9
+ customRowInputModalities,
10
+ isModelVisionSidecarConsumer as isModelTextOnly,
11
+ modelAcceptsImageInput,
12
+ } from "./eligibility";
9
13
  import { normalizeVisionReasoningForModel } from "./reasoning";
10
14
  import { resolveSidecarAuth } from "../sidecar/auth";
11
15
  import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds";
@@ -95,23 +99,40 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
95
99
  /**
96
100
  * Direct-image admission for a routed target. Returns true when capability evidence proves the
97
101
  * target cannot accept image input, so the caller must describe or strip the image first.
98
- * Explicit text-only config, an explicit per-model modality list without `image`, and
99
- * proven-negative registry/vendor metadata each require the vision preprocessor. A genuinely
100
- * unknown custom model is NOT guessed blind: it keeps the established pass-through behaviour.
101
- * The provider-only fallback keeps legacy unit callers stable; production dispatch always
102
- * supplies providerName so the complete capability chain is consulted.
102
+ * Evidence is consulted highest-first: `modelCapabilities` (the dedicated per-model capability
103
+ * axis), an explicit custom row for the same routed identity, `noVisionModels`, an explicit
104
+ * per-model modality list without `image`, and finally proven-negative registry/vendor metadata.
105
+ * Any of them can require the vision preprocessor. A genuinely unknown custom model is NOT
106
+ * guessed blind: it keeps the established pass-through behaviour. The provider-only fallback
107
+ * keeps legacy unit callers stable; production dispatch always supplies providerName so the
108
+ * complete capability chain is consulted.
103
109
  */
104
110
  export function requiresVisionPreprocessing(
105
- config: Pick<OcxConfig, "providers">,
111
+ config: Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] },
106
112
  provider: Pick<OcxProviderConfig, "noVisionModels" | "modelInputModalities" | "modelCapabilities">,
107
113
  modelId: string,
108
114
  providerName?: string,
109
115
  ): boolean {
110
- if (isModelTextOnly(provider, modelId)) return true;
111
116
  const runtimeDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
112
117
  ? provider.modelCapabilities?.[modelId]?.inputModalities
113
118
  : undefined;
119
+ // `modelCapabilities` is the dedicated per-model capability axis, so it outranks everything
120
+ // below — including an explicit custom row that disagrees. The CLI `--text-only` flag writes it.
114
121
  if (runtimeDeclared !== undefined) return !runtimeDeclared.includes("image");
122
+ // An explicit custom row outranks the provider-level hints below, mirroring the catalog overlay
123
+ // in `src/codex/catalog/routed-gather.ts` that already copies this same declaration onto the
124
+ // advertised row. Without it the dashboard advertised "text, image" from the operator's own row
125
+ // while this predicate stripped the image before dispatch.
126
+ const customDeclared = providerName === undefined
127
+ ? undefined
128
+ : customRowInputModalities(config, providerName, modelId);
129
+ // Same rule as the `modelCapabilities` branch above: the declaration answers "can this model
130
+ // take an image", not "is it a text model". A row that lists only `audio` or `video` excludes
131
+ // image input just as `["text"]` does, and `modelAcceptsImageInput` already answers "no image"
132
+ // for it; the narrower `includes("text")` test here made the two predicates disagree and sent
133
+ // the attachment to a model that cannot read it.
134
+ if (customDeclared !== undefined) return !customDeclared.includes("image");
135
+ if (isModelTextOnly(provider, modelId)) return true;
115
136
  const runtimeModalities = modelRecordValue(provider.modelInputModalities, modelId);
116
137
  if (Array.isArray(runtimeModalities) && runtimeModalities.length > 0) {
117
138
  return !runtimeModalities.includes("image");
@@ -129,9 +150,12 @@ function usableRoutedVisionModel(config: OcxConfig): string | undefined {
129
150
  if (!routedModel || sep <= 0) return undefined;
130
151
  const targetProvider = routedModel.slice(0, sep);
131
152
  const targetId = routedModel.slice(sep + 1);
132
- const targetProviderConfig = config.providers?.[targetProvider];
153
+ // `modelAcceptsImageInput` is the one predicate here: it resolves the whole capability chain,
154
+ // custom row included. The provider-only `isModelTextOnly` that used to be ANDed in could only
155
+ // subtract — it never saw a custom row, so a describer the operator had declared image-capable
156
+ // was refused for a provider hint that same row overrides.
133
157
  return modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
134
- && !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId)) ? routedModel : undefined;
158
+ ? routedModel : undefined;
135
159
  }
136
160
 
137
161
  export function shouldResolveOpenAiVisionSidecar(
@@ -14,6 +14,8 @@
14
14
  import { formatErrorResponse } from "../bridge";
15
15
  import { redactSecretString } from "../lib/redact";
16
16
  import { sidecarEnter } from "../lib/sidecar-tracker";
17
+ import { admissionScopeDenial } from "../server/admission-model-scope";
18
+ import type { DataPlaneAdmission } from "../server/auth-cors";
17
19
  import type { OcxConfig, OcxProviderConfig, OcxWebSearchSidecarConfig } from "../types";
18
20
  import { runAnthropicWebSearch } from "./anthropic-executor";
19
21
  import { runExaWebSearch } from "./exa-executor";
@@ -252,6 +254,7 @@ export async function handleAlphaSearchSidecarFallback(
252
254
  config: OcxConfig,
253
255
  signal?: AbortSignal,
254
256
  logCtx?: { provider: string },
257
+ admission?: DataPlaneAdmission,
255
258
  ): Promise<Response> {
256
259
  const resolution = resolveAlphaSearchSidecar(config);
257
260
  if (resolution.status === "missing-credential") {
@@ -266,6 +269,24 @@ export async function handleAlphaSearchSidecarFallback(
266
269
  const resolved = resolution.sidecar;
267
270
  if (logCtx) logCtx.provider = resolved.backend;
268
271
 
272
+ // This backend is a paid destination like any other, and the operator's
273
+ // configuration -- not the caller -- decides which one and which model. A key
274
+ // scoped away from it must not spend it by asking the search endpoint instead
275
+ // of the inference one. Exa has no configured provider entry, so its own
276
+ // backend name is the destination.
277
+ const settings = sidecarSettingsForAlphaSearch(resolved.backend, config);
278
+ const requestedModel = (body as { model?: unknown } | null)?.model;
279
+ const denial = admissionScopeDenial(
280
+ config,
281
+ admission,
282
+ typeof requestedModel === "string" && requestedModel.trim() ? requestedModel : undefined,
283
+ {
284
+ providerName: resolved.backend === "exa" ? resolved.backend : resolved.providerName,
285
+ modelId: settings.model,
286
+ },
287
+ );
288
+ if (denial) return denial;
289
+
269
290
  const queries = extractAlphaSearchQueries(body);
270
291
  if (queries.length === 0) {
271
292
  return formatErrorResponse(
@@ -275,7 +296,6 @@ export async function handleAlphaSearchSidecarFallback(
275
296
  );
276
297
  }
277
298
 
278
- const settings = sidecarSettingsForAlphaSearch(resolved.backend, config);
279
299
  const sidecarExit = sidecarEnter("search");
280
300
  try {
281
301
  const texts: string[] = [];
@@ -3,7 +3,14 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
3
3
  import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort";
4
4
  import { redactSecretString } from "../lib/redact";
5
5
  import { sidecarEnter } from "../lib/sidecar-tracker";
6
- import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry";
6
+ import {
7
+ applyUpstreamRecoveryInit,
8
+ fetchWithResetRetry,
9
+ releaseResponseBodyBestEffort,
10
+ retryBackoffDelayMs,
11
+ sleepWithAbort,
12
+ RETRY_AFTER_CEILING_MS,
13
+ } from "../lib/upstream-retry";
7
14
  import { withUpstreamHttpVersion } from "../lib/upstream-http-version";
8
15
  import { parseSidecarSSE, type WebSearchResult } from "./parse";
9
16
  import type { CodexUpstreamOutcome } from "../codex/routing";
@@ -35,6 +42,22 @@ export const IMAGE_INSTRUCTION =
35
42
 
36
43
  /** A search result, or an `error` string when the search couldn't run (surfaced as a tool result). */
37
44
  export type SidecarOutcome = WebSearchResult & { error?: string };
45
+
46
+ /**
47
+ * Bounded same-search 429 replays for the sidecar POST.
48
+ *
49
+ * The forward backend throttles burst sidecar traffic, and without a replay the 429 becomes a
50
+ * failed tool result that poisons the query for the whole turn (see failedQueries in loop.ts).
51
+ * 1 initial send + 2 replays; Retry-After is honored as a lower bound and capped by
52
+ * RETRY_AFTER_CEILING_MS (an instruction past the ceiling ends with the 429 instead of
53
+ * parking the search). Each wait releases the unread 429 body first so sockets do not
54
+ * accumulate under a rate-limit storm. Abort or timeout ends the wait through the existing
55
+ * catch, exactly like an abort during the SSE parse.
56
+ */
57
+ const SIDECAR_429_MAX_ATTEMPTS = 3;
58
+ const SIDECAR_429_BASE_DELAY_MS = 1_000;
59
+ const SIDECAR_429_MAX_DELAY_MS = 10_000;
60
+
38
61
  export type SidecarOutcomeRecorder = (outcome: CodexUpstreamOutcome) => void;
39
62
 
40
63
  /**
@@ -79,7 +102,7 @@ export async function runWebSearch(
79
102
  const sidecarExit = sidecarEnter("web-search");
80
103
  const t0 = Date.now();
81
104
  try {
82
- const res = await fetchWithResetRetry(
105
+ const sendOnce = () => fetchWithResetRetry(
83
106
  // Recovery nests INSIDE the version helper: applyUpstreamRecoveryInit then always receives a
84
107
  // defined init, and withUpstreamHttpVersion spreads the result, so `protocol` and the
85
108
  // recovery fields (`connection: close` + Bun's transport-level `keepalive: false`) survive
@@ -96,6 +119,22 @@ export async function runWebSearch(
96
119
  }, recovery), forwardProvider)),
97
120
  { replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
98
121
  );
122
+ let res = await sendOnce();
123
+ for (let attempt = 0; res.status === 429 && attempt + 1 < SIDECAR_429_MAX_ATTEMPTS; attempt++) {
124
+ const delay = retryBackoffDelayMs(attempt, {
125
+ baseDelayMs: SIDECAR_429_BASE_DELAY_MS,
126
+ maxDelayMs: SIDECAR_429_MAX_DELAY_MS,
127
+ headers: res.headers,
128
+ retryAfterIsLowerBound: true,
129
+ });
130
+ // A deadline, not a clamp: an instruction past the ceiling ends the search with the
131
+ // 429 instead of parking it at a provider that already said it would refuse.
132
+ if (delay > RETRY_AFTER_CEILING_MS) break;
133
+ console.warn(`[web-search] sidecar HTTP 429 — retrying (${attempt + 2}/${SIDECAR_429_MAX_ATTEMPTS}) after ${delay}ms`);
134
+ await releaseResponseBodyBestEffort(res.body, linkedSignal.signal);
135
+ await sleepWithAbort(delay, linkedSignal.signal);
136
+ res = await sendOnce();
137
+ }
99
138
  // Attach the body guard before ANY branch reads it. The success path guarded itself below,
100
139
  // but the failure branch's `res.text()` runs first, so a cancel landing between fetch
101
140
  // resolution and reader attach orphaned the internal rejection (found investigating #1419).
@@ -333,10 +333,15 @@ export interface WebSearchLoopDeps {
333
333
  * 429 states the window's reset epoch even when it omits Retry-After -- and a rotation that
334
334
  * cannot see it cools the drained account for the short default instead of until the window
335
335
  * actually reopens. Optional so existing callers keep compiling.
336
+ *
337
+ * `retryParsed` is the exact iteration-local request the retry will be built from. The loop
338
+ * sends a shallow copy of the outer parsed request, so a rotation that rebinds only the outer
339
+ * object never reaches the wire. Optional so existing callers keep compiling.
336
340
  */
337
341
  on429?: (
338
342
  retryAfterHeader: string | null,
339
343
  responseHeaders?: Headers,
344
+ retryParsed?: OcxParsedRequest,
340
345
  ) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
341
346
  /** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
342
347
  retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
@@ -558,7 +563,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
558
563
  // 429 key-failover parity with the normal routed path: rotate pool keys until one responds
559
564
  // or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
560
565
  while (prepared.response.status === 429 && deps.on429) {
561
- const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers);
566
+ const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers, iterParsed);
562
567
  if (!rotated) break;
563
568
  // Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
564
569
  // it, but proceed immediately to the rotated fetch under the SAME deadline signal.