@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
@@ -12,9 +12,57 @@ import { waitForProviderRequestSlot } from "../../providers/request-pacing";
12
12
  import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
13
13
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
14
14
  import { configuredOutboundFetch } from "../../lib/proxy-env";
15
+ import {
16
+ describeProviderEgressForLog,
17
+ markEgressTransparentExecutor,
18
+ providerEgressSendInit,
19
+ providerEgressIsExplicit,
20
+ resolveProviderEgress,
21
+ type ProviderEgressBinding,
22
+ } from "../../lib/provider-egress";
23
+ import { redactSecretString } from "../../lib/redact";
15
24
 
16
25
  export { withUpstreamHttpVersion };
17
26
 
27
+ const egressWebsocketDowngradeWarned = new Set<string>();
28
+ /** A provider name is configuration-controlled, so the notice set is bounded like any cache. */
29
+ const EGRESS_DOWNGRADE_NOTICE_LIMIT = 64;
30
+ /**
31
+ * Marks an init whose provider egress route an outer physical-send boundary already decided.
32
+ *
33
+ * Own symbol keys survive object spread, so the mark travels through the rebuild a
34
+ * `dispatchOverride` performs, and an unknown symbol on a `RequestInit` is inert at the wire.
35
+ */
36
+ const EGRESS_DECIDED = Symbol.for("opencodex.provider-egress.decided");
37
+
38
+ /**
39
+ * Announce once, per provider, that an explicit egress route moved this provider off the
40
+ * WebSocket fast lane.
41
+ *
42
+ * The WebSocket upstream selects its proxy from the process environment when it dials, so it
43
+ * cannot carry a per-provider route. Serving the turn over HTTP/SSE honours the operator's
44
+ * egress choice, which is the one that has to win — but a transport change the operator did
45
+ * not ask for is exactly the kind of substitution this batch refuses to make silently, so it
46
+ * is stated rather than merely done.
47
+ */
48
+ function warnEgressWebsocketDowngradeOnce(providerName: string, egress: string): void {
49
+ if (egressWebsocketDowngradeWarned.has(providerName)) return;
50
+ if (egressWebsocketDowngradeWarned.size >= EGRESS_DOWNGRADE_NOTICE_LIMIT) return;
51
+ egressWebsocketDowngradeWarned.add(providerName);
52
+ console.warn(
53
+ // The name is caller-controlled and can be token-shaped, so it is redacted and JSON-escaped
54
+ // before it reaches a log, exactly as at the management error boundary.
55
+ `[opencodex] provider ${JSON.stringify(redactSecretString(providerName))} declares egress ${egress}; the WebSocket upstream `
56
+ + "selects its proxy from the process environment and cannot carry a per-provider route, "
57
+ + "so these turns are served over HTTP/SSE.",
58
+ );
59
+ }
60
+
61
+ /** Test seam: the downgrade notice is once per provider per process, not once per request. */
62
+ export function __resetEgressWebsocketDowngradeNotices(): void {
63
+ egressWebsocketDowngradeWarned.clear();
64
+ }
65
+
18
66
  export function disableResponsesRequestTimeout(req: Request, server: Pick<Server<WsData>, "timeout"> | undefined): boolean {
19
67
  if (!server) return false;
20
68
  try {
@@ -97,21 +145,43 @@ export function sendWithConnectionPolicy(
97
145
  physicalFetch: typeof globalThis.fetch,
98
146
  input: Parameters<typeof globalThis.fetch>[0],
99
147
  init?: RequestInit,
148
+ egress?: ProviderEgressBinding,
100
149
  ): Promise<Response> {
101
150
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
102
151
  const fresh = wantsFreshConnection(input);
103
152
  if (fresh) {
104
153
  headers.set("Connection", "close");
105
154
  }
155
+ // Decided here, against the destination this send is actually going to, and around whichever
156
+ // executor was just selected. A `dispatchOverride` that rebuilds a queued request can change
157
+ // both the upstream host and the provider transport after the wrapper was constructed, so a
158
+ // route resolved at construction could be applied to a different host than it was decided for.
159
+ // These calls nest: an override decides with its own binding and then hands the send to the
160
+ // executor `providerFetch` supplied, which is another one of these. The outermost caller holds
161
+ // the reselected provider and the rebuilt destination, so it decides and marks the init; the
162
+ // inner pass honours that mark rather than recomputing from a stale closure.
163
+ const alreadyDecided = (init as Record<symbol, unknown> | undefined)?.[EGRESS_DECIDED] === true;
164
+ const decide = egress !== undefined && !alreadyDecided;
165
+ const egressInit = decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
106
166
  return physicalFetch(input, {
107
167
  ...init,
108
168
  headers,
109
169
  redirect: "manual",
110
170
  ...(fresh ? { keepalive: false } : {}),
171
+ ...egressInit,
172
+ ...(decide ? { [EGRESS_DECIDED]: true } : {}),
111
173
  });
112
174
  }
113
175
 
114
176
  export interface ProviderFetchOptions {
177
+ /**
178
+ * Keep this send on HTTP even where the WebSocket upstream would normally be selected.
179
+ *
180
+ * Set by a caller replacing an HTTP stream that already failed: a WS create frame is a
181
+ * different send on a different transport, and the replacement has to be the same kind of
182
+ * exchange the client is already reading.
183
+ */
184
+ httpOnly?: boolean;
115
185
  nativeControl?: NativeResponseControl;
116
186
  providerName?: string;
117
187
  modelId?: string;
@@ -130,28 +200,63 @@ export function providerFetch(
130
200
  runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
131
201
  options: ProviderFetchOptions = {},
132
202
  ): ProviderFetch {
133
- const configuredFetch = Object.assign(
203
+ const providerName = options.providerName ?? "<unnamed provider>";
204
+ const customExecutor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
205
+ // The route is applied at the physical send (see `sendWithConnectionPolicy`). This binding is
206
+ // only what that boundary needs to decide it.
207
+ const egressBinding: ProviderEgressBinding = { providerName, provider };
208
+ // Resolved per request, not once per wrapper: `providers.<name>.noProxy` is evaluated against
209
+ // the destination, so two requests through the same executor can legitimately take different
210
+ // routes. A malformed value throws and rejects the request rather than degrading to the
211
+ // global proxy or to direct, either of which would read as success at the call site.
212
+ const egressFor = (input: Parameters<typeof globalThis.fetch>[0]) => resolveProviderEgress({
213
+ providerName,
214
+ provider,
215
+ url: typeof input === "string" ? input : input instanceof URL ? input : input.url,
216
+ });
217
+ // The built-in executor forwards its init to a transport that honours the proxy option.
218
+ const configuredFetch = markEgressTransparentExecutor(Object.assign(
134
219
  (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => configuredOutboundFetch(input, init),
135
220
  { preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) },
136
- ) as typeof globalThis.fetch;
137
- const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? configuredFetch;
221
+ ) as typeof globalThis.fetch);
222
+ const base = customExecutor ?? configuredFetch;
138
223
  const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
139
224
  base.preconnect?.(...args);
140
225
  };
141
226
  // Rebuilt dispatches must use the same physical-send boundary as ordinary HTTP sends.
142
227
  // Return the original 3xx so the response owner retains its retry/health/relay contract.
143
- const dispatch = Object.assign(
228
+ //
229
+ // Marked transparent because it forwards its init to a transport that honours the proxy
230
+ // option. Leaving it unmarked would make an ordinary configured provider refuse its own route
231
+ // on every overridden path, after the attempt had already been recorded — an override selects
232
+ // `provider.fetch ?? execute`, and `execute` is this wrapper. It still carries the binding, so
233
+ // an override that simply calls it gets the route decided rather than dropped; an override
234
+ // that decided for itself has already marked the init and this pass defers to that decision.
235
+ const dispatch = markEgressTransparentExecutor(Object.assign(
144
236
  (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
145
- sendWithConnectionPolicy(base, input, init),
237
+ sendWithConnectionPolicy(base, input, init, egressBinding),
146
238
  { preconnect },
147
- ) as typeof globalThis.fetch;
239
+ ) as typeof globalThis.fetch);
148
240
  const httpFetch = Object.assign(
149
241
  async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
242
+ // Refuse before any dispatch side effect where that is sound. `beforeDispatch` commits
243
+ // attempt accounting and consumes admission state, so a refusal firing after it would
244
+ // charge an attempt for a send that never happens, and a throwing hook would mask the
245
+ // egress error with an unrelated one.
246
+ //
247
+ // With no override, this input and `base` ARE the final destination and executor, so the
248
+ // full decision can be made now. With an override, only the configured value is checked:
249
+ // the override may rebuild against a different host and select a different transport, and
250
+ // refusing on this destination would reject a request whose real route is fine.
251
+ if (options.dispatchOverride) egressFor(input);
252
+ else providerEgressSendInit(egressBinding, base, input);
150
253
  // The hook inspects the outgoing headers and refuses the send by throwing; it is not a
151
254
  // mutator, and the copy it receives is deliberately not threaded onward. `Connection`
152
255
  // is decided inside `dispatch`, which runs after this, so the fresh-connection policy
153
256
  // wins regardless of what any caller or hook put in the header.
154
257
  options.beforeDispatch?.(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)));
258
+ // No proxy option is attached here: a `dispatchOverride` may rebuild this request against
259
+ // a different destination, so the route is decided at the physical send instead.
155
260
  const dispatchInit = { ...withUpstreamHttpVersion(input, init, provider), timeout: 0 };
156
261
  return options.dispatchOverride
157
262
  ? options.dispatchOverride(input, dispatchInit, dispatch)
@@ -164,7 +269,13 @@ export function providerFetch(
164
269
  // else keeps the provider's HTTP fetch. See ws-upstream.ts for the details.
165
270
  const unpaced = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
166
271
  const upstreamWebsocket = provider.upstreamWebsocket === true;
167
- if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init, runtime, upstreamWebsocket)) {
272
+ if (!options.httpOnly && typeof input === "string" && init
273
+ && shouldUseCodexWsUpstream(input, init, runtime, upstreamWebsocket)) {
274
+ const egress = egressFor(input);
275
+ if (providerEgressIsExplicit(egress)) {
276
+ warnEgressWebsocketDowngradeOnce(providerName, describeProviderEgressForLog(egress));
277
+ return httpFetch(input, init);
278
+ }
168
279
  // The fallback has to be the same HTTP fetch the non-WS branch would have
169
280
  // used, protocol pin included: a WS turn that falls back is serving the
170
281
  // request over HTTP, and dropping the provider's `upstreamHttpVersion`
@@ -188,11 +299,16 @@ export function providerFetch(
188
299
  await waitForPacing(init?.signal ?? undefined);
189
300
  return unpaced(input, init);
190
301
  };
191
- return Object.assign(wrapped, {
302
+ // The returned wrapper forwards its init down to `dispatch`, which applies the route at the
303
+ // physical send. Adapters that hand this executor back as `provider.fetch` (Cursor does)
304
+ // therefore still carry a per-provider route instead of being refused as opaque.
305
+ const paceAware = Object.assign(wrapped, {
192
306
  preconnect,
193
307
  waitForPacing,
194
308
  unpacedFetch: Object.assign(unpaced, { preconnect }),
195
309
  });
310
+ markEgressTransparentExecutor(paceAware as unknown as typeof globalThis.fetch);
311
+ return paceAware;
196
312
  }
197
313
 
198
314
 
@@ -85,9 +85,19 @@ function imageTokens(imageUrl: string): number {
85
85
  function contentPartTokens(part: OcxContentPart, modelId: string): number {
86
86
  if (part.type === "image") return imageTokens(part.imageUrl);
87
87
  if (part.type === "video") return imageTokens(part.videoUrl);
88
+ // An inline document is a base64 payload, not a sentence: estimating it from its marker would
89
+ // admit a request whose real input is orders of magnitude larger. Counted arithmetically —
90
+ // rebuilding the data URL here would materialize a second request-sized string just to measure it.
91
+ if (part.type === "document") return base64PayloadTokens(part.data);
88
92
  return estimateTokens(part.text, modelId);
89
93
  }
90
94
 
95
+ function base64PayloadTokens(base64: string): number {
96
+ if (base64.length === 0) return 0;
97
+ const decoded = Math.floor((base64.length * 3) / 4);
98
+ return Math.max(1, Math.ceil(decoded / IMAGE_BYTES_PER_TOKEN));
99
+ }
100
+
91
101
  function contentTokens(content: string | readonly OcxContentPart[], modelId: string): number {
92
102
  if (typeof content === "string") return estimateTokens(content, modelId);
93
103
  let total = 0;
@@ -35,10 +35,6 @@ import { consumeComboFailure } from "./core-combo-failure";
35
35
  import { readDisplaySafeErrorText } from "./core-errors";
36
36
  import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
37
37
  import { formatPassthroughUpstreamError } from "./passthrough-error";
38
- import {
39
- providerModelResponsesTerminalRepair,
40
- providerModelResponsesUpstreamStreaming,
41
- } from "../../providers/registry";
42
38
  import {
43
39
  resolvePassthroughWebSearchBridgeAuth,
44
40
  planPassthroughWebSearchBridge,
@@ -83,7 +79,10 @@ import {
83
79
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
84
80
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
85
81
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
86
- import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
82
+ import {
83
+ createGrokResponsesControlFrameBlockRewrite,
84
+ createGrokResponsesTimestampBlockRewrite,
85
+ } from "../grok-responses-control-frame";
87
86
  import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
88
87
  import {
89
88
  createPlaintextV2AgentMessageCallRestoreRewrite,
@@ -94,11 +93,30 @@ import { createResponsesFieldBackfillBlockRewrite } from "./responses-field-back
94
93
  import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
95
94
  import {
96
95
  createUndeclaredToolCallGuardBlockRewrite,
96
+ currentTurnWireToolCatalogBody,
97
97
  undeclaredToolCallNameInResponse,
98
98
  undeclaredToolCallMessage,
99
99
  normalizeDefaultNamespaceInJson,
100
100
  } from "../responses-undeclared-tool-guard";
101
101
  import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
102
+
103
+ /**
104
+ * Platform override for the two relay-path policy calls below. Tests only.
105
+ *
106
+ * The eager relay is reachable only on win32 and darwin, so a Linux shard cannot exercise it
107
+ * without claiming to be one of them. Overwriting `process.platform` globally does that, and a
108
+ * great deal more: every filesystem, ACL and state-directory decision in the process follows it,
109
+ * and the spend-ledger owner lowercases its home on win32, which on a case-sensitive filesystem
110
+ * names a DIFFERENT directory. A row that did that stopped being able to reserve its send and
111
+ * delivered no terminal at all, reporting as a relay defect. This narrows the claim to the two
112
+ * calls that actually choose the relay path.
113
+ */
114
+ let relayPlatformForTests: NodeJS.Platform | undefined;
115
+
116
+ /** Internal test contract, not operator configuration: no config key reaches this. */
117
+ export function setRelayPlatformForTests(platform: NodeJS.Platform | undefined): void {
118
+ relayPlatformForTests = platform;
119
+ }
102
120
  import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
103
121
  import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
104
122
  import { relaySseEagerBounded } from "../relay-eager";
@@ -340,11 +358,7 @@ export async function deliverPassthroughResponse(
340
358
  // before relay starts. Waiting for SSE completion would retain request state across the whole
341
359
  // stream; a later body failure does not undo that this destination accepted and served the turn.
342
360
  commitReasoningReplayServingRoute(nativeExchange.request.headers);
343
- const terminalRepairPolicy = providerModelResponsesTerminalRepair(
344
- route.providerName,
345
- route.provider,
346
- route.modelId,
347
- );
361
+ const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
348
362
  // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
349
363
  // and this branch relays that declaration on the assumption the destination executes it.
350
364
  // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
@@ -499,7 +513,22 @@ export async function deliverPassthroughResponse(
499
513
  ? createGrokResponsesControlFrameBlockRewrite()
500
514
  : undefined,
501
515
  grokClientCompatibilityEnabled
502
- ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
516
+ ? createGrokResponsesTimestampBlockRewrite()
517
+ : undefined,
518
+ grokClientCompatibilityEnabled
519
+ ? createGrokResponsesSparseTerminalBlockRewrite(
520
+ translatorBudget,
521
+ nativeExchange.outboundRequestBody,
522
+ {
523
+ clientToolAuthorizationBody: currentTurnWireToolCatalogBody(
524
+ parsed._rawBody,
525
+ parsed._replayPrefixLen ?? 0,
526
+ ),
527
+ routedNamespaceToolAliases: responseEffects.routedNamespaceToolAliases,
528
+ routedMuseToolNameAliases: responseEffects.routedMuseToolNameAliases,
529
+ convertedRoutedCustomToolNames: routedCustomToolNames,
530
+ },
531
+ )
503
532
  : undefined,
504
533
  snapshotRepairEnabled
505
534
  ? createResponsesSnapshotBlockRewrite(nativeExchange.outboundRequestBody, translatorBudget)
@@ -529,12 +558,13 @@ export async function deliverPassthroughResponse(
529
558
  ? composeSseBlockRewrites(...blockRewrites)
530
559
  : undefined;
531
560
  const needsClientRewrite = clientBlockRewrite !== undefined;
561
+ const relayPlatform = relayPlatformForTests ?? process.platform;
532
562
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
533
563
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
534
564
  // lost). The eager single reader applies the same rewrites inline.
535
- const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
565
+ const win32EagerRewrite = isWin32EagerRewrite(relayPlatform, needsClientRewrite);
536
566
  const eagerPath = selectEagerPath(
537
- process.platform,
567
+ relayPlatform,
538
568
  needsClientRewrite,
539
569
  config.streamMode ?? "auto",
540
570
  );
@@ -805,7 +835,7 @@ export async function deliverPassthroughResponse(
805
835
  // stream that never closes. Non-streaming clients keep the plain JSON.
806
836
  if (clientRequestedStream === true
807
837
  && options.inboundTransport !== "websocket"
808
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
838
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
809
839
  && route.provider.adapter === "openai-responses") {
810
840
  let completed: Record<string, unknown> | undefined;
811
841
  try {
@@ -852,7 +882,7 @@ export async function deliverPassthroughResponse(
852
882
  // WS turns reframe this JSON into events in the bridge, which is the
853
883
  // other relay-free path — normalize ids so both bounded-JSON paths agree.
854
884
  const outboundJson = options.inboundTransport === "websocket"
855
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
885
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
856
886
  && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
857
887
  ? (() => {
858
888
  try {