@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
@@ -61,6 +61,9 @@ export function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined
61
61
  parameters: normalizeParameters(t.parameters),
62
62
  };
63
63
  if (t.strict !== undefined) tool.strict = t.strict as boolean;
64
+ if (Array.isArray(t.allowed_callers)) {
65
+ tool.allowedCallers = (t.allowed_callers as unknown[]).filter((c): c is string => typeof c === "string");
66
+ }
64
67
  if (namespace) tool.namespace = namespace;
65
68
  out.push(tool);
66
69
  };
@@ -245,7 +245,9 @@ export function parseRequest(
245
245
  case "system": {
246
246
  pendingReasoning.length = 0;
247
247
  const text = inputContentParts(msg.content);
248
- const flat = typeof text === "string" ? text : text.map(p => (p.type === "text" ? p.text : "")).join("");
248
+ const flat = typeof text === "string"
249
+ ? text
250
+ : text.map(p => (p.type === "text" || p.type === "document" ? p.text : "")).join("");
249
251
  if (flat.length > 0) systemPrompt.push(flat);
250
252
  break;
251
253
  }
@@ -0,0 +1,130 @@
1
+ import { unwrapFreeformToolInput } from "./apply-patch-envelope";
2
+ import { JSON_ESCAPES, scanFreeformWrapper } from "./freeform-wrapper-scan";
3
+
4
+ /**
5
+ * Whether a body could still grow into one complete outer Markdown fence.
6
+ *
7
+ * `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
8
+ * own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
9
+ * the stream holds. A buffer that does not open with a fence can never acquire one, so
10
+ * ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
11
+ * whole call, because a closing fence can still be followed by more text that withdraws it.
12
+ */
13
+ function mayBecomeFencedBody(text: string, toolName: string): boolean {
14
+ if (toolName !== "exec" && toolName !== "apply_patch") return false;
15
+ const head = text.trimStart();
16
+ if (head === "") return true;
17
+ return head.startsWith("```") || "```".startsWith(head);
18
+ }
19
+
20
+ const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
21
+
22
+ /**
23
+ * The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
24
+ *
25
+ * Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
26
+ * and anything invented here would be retracted. Three of them are not obvious:
27
+ *
28
+ * An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
29
+ * next, so completion falls back to the raw text. Returning `null` for that case stops the
30
+ * preview rather than continuing to decode a value the completed item will never carry.
31
+ * `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
32
+ *
33
+ * A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
34
+ * delta the client has to decode by itself, so the pair is emitted together or not at all.
35
+ */
36
+ function decodeJsonStringPrefix(body: string): string | null {
37
+ let out = "";
38
+ for (let i = 0; i < body.length; i++) {
39
+ const c = body[i];
40
+ if (c === '"') break; // unescaped closing quote: value complete
41
+ if (c !== "\\") {
42
+ // A literal control character is not legal inside a JSON string, so `JSON.parse` will
43
+ // reject this wrapper and completion will return the raw text. Emitting the decoded value
44
+ // first is the disagreement this decoder exists to prevent, so stop instead.
45
+ if (c!.charCodeAt(0) <= 0x1f) return null;
46
+ out += c;
47
+ continue;
48
+ }
49
+ const n = body[i + 1];
50
+ if (n === undefined) break; // escape split across chunks: wait for more
51
+ if (n === "u") {
52
+ const hex = body.slice(i + 2, i + 6);
53
+ if (hex.length < 4) break; // split across chunks: wait for more
54
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
55
+ const code = parseInt(hex, 16);
56
+ if (code >= 0xd800 && code <= 0xdbff) {
57
+ const low = body.slice(i + 6, i + 12);
58
+ if (!LOW_SURROGATE_ESCAPE.test(low)) break;
59
+ out += String.fromCharCode(code, parseInt(low.slice(2), 16));
60
+ i += 11;
61
+ continue;
62
+ }
63
+ out += String.fromCharCode(code);
64
+ i += 5;
65
+ continue;
66
+ }
67
+ const escaped = JSON_ESCAPES.get(n);
68
+ if (escaped === undefined) return null; // never parses
69
+ out += escaped;
70
+ i += 1;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * The value to stream so far, or `null` to HOLD because nothing can be decided yet.
77
+ *
78
+ * `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
79
+ * key is present, whatever else the object carries, so its value can be unescaped
80
+ * progressively and never retracted.
81
+ *
82
+ * One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
83
+ * `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
84
+ * b. Closing it means holding every canonical wrapper until its object parses, which is the
85
+ * progressive streaming this path exists to provide. The completed item stays authoritative,
86
+ * and no model emits a duplicate key in practice.
87
+ *
88
+ * A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
89
+ * answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
90
+ * arrives, and completion returns the raw text because nothing parses. The preview stops
91
+ * there: no rewind, and no decoded text the completed item does not contain. Bounding the
92
+ * damage is what is available without giving up progressive streaming, and the args are
93
+ * unusable in that case whichever representation wins.
94
+ *
95
+ * A fallback key is not decidable. It only unwraps when it is the SINGLE string field, and a
96
+ * second key can still arrive — so a value emitted early would have to be taken back. That is
97
+ * the rewind this holds instead: stream nothing until the object closes, then publish the one
98
+ * repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
99
+ * any object prefix for the same reason (#5047).
100
+ *
101
+ * An object that has not reached a canonical key YET is in exactly that position, and used to
102
+ * be treated as raw because it did not match the literal `{"input":"`. It holds now: `input`
103
+ * can still arrive after other properties, or wearing an escaped spelling, and completion would
104
+ * then unwrap a body whose wrapper syntax had already been published as deltas (#5151). The
105
+ * cost of holding is preview on an object body that turns out not to be a wrapper; the cost of
106
+ * not holding was publishing bytes the completed item removes.
107
+ */
108
+ export function progressiveFreeformInput(args: string, toolName: string): string | null {
109
+ const scan = scanFreeformWrapper(args);
110
+ if (scan.kind === "input") {
111
+ const decoded = decodeJsonStringPrefix(args.slice(scan.valueStart));
112
+ if (decoded === null) return null;
113
+ return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
114
+ }
115
+ if (scan.kind === "raw") return mayBecomeFencedBody(args, toolName) ? null : args;
116
+
117
+ // Undecided: some wrapper may still apply, and only the completed object says which one.
118
+ // The parse runs exactly where the scan SAW the object close, so it reads a buffer the scan
119
+ // already walked and its cost is bounded by the same clamp. A hold from either limit stays
120
+ // held: an incomplete object has nothing to parse, and re-reading a budget-exhausted buffer
121
+ // on every delta is quadratic work for a delta that would arrive in the same instant as the
122
+ // authoritative completion behind it.
123
+ if (!scan.parse) return null;
124
+ try {
125
+ JSON.parse(args);
126
+ } catch {
127
+ return null;
128
+ }
129
+ return unwrapFreeformToolInput(args, toolName);
130
+ }
@@ -63,6 +63,36 @@ export function encodeReasoningEnvelope(envelope: ReasoningEnvelope, budget?: Tr
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * Whether a thinking part's `signature` is an attestation some provider issued, rather than
68
+ * the serialized reasoning item the parser parks in the same field.
69
+ *
70
+ * `src/responses/parser.ts` stores `JSON.stringify(reasoningItem)` on an UNSIGNED thinking
71
+ * part so the opaque item survives a same-provider round trip. Every adapter that forwards a
72
+ * signature upstream is forwarding an opaque attestation, and a provider asked to verify our
73
+ * own parser state cannot recognize it. The predicate lives beside the envelope code that owns
74
+ * this field's representation rather than in one adapter, because a private second copy is how
75
+ * the side that writes the field and a side that reads it come to disagree about what it holds.
76
+ *
77
+ * This is a deny-list for exactly that one shape, not a guess at what an attestation looks
78
+ * like. `src/adapters/anthropic.ts` applies a stricter allow-list on top of it, because an
79
+ * Anthropic signature has a known base64 spelling; that shape is a fact about Anthropic's wire
80
+ * and is not assumed of any other provider's token here.
81
+ */
82
+ export function isProviderIssuedThinkingSignature(
83
+ signature: string | undefined,
84
+ ): signature is string {
85
+ if (typeof signature !== "string" || signature.length === 0) return false;
86
+ if (!signature.startsWith("{")) return true;
87
+ try {
88
+ const parsed: unknown = JSON.parse(signature);
89
+ return !parsed || typeof parsed !== "object" || Array.isArray(parsed)
90
+ || (parsed as { type?: unknown }).type !== "reasoning";
91
+ } catch {
92
+ return true;
93
+ }
94
+ }
95
+
66
96
  /** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
67
97
  export function decodeReasoningEnvelope(encryptedContent: string, budget?: TranslatorBudget): ReasoningEnvelope | null {
68
98
  if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
@@ -122,6 +122,9 @@ export const toolSchema = z.object({
122
122
  description: z.string().optional(),
123
123
  parameters: z.record(z.string(), z.unknown()).optional(),
124
124
  strict: z.boolean().optional(),
125
+ // Unknown keys are stripped here, so a field the parser is expected to read has to be
126
+ // declared: an undeclared allowed_callers never reached buildTools at all (#5210).
127
+ allowed_callers: z.array(z.string()).optional(),
125
128
  });
126
129
 
127
130
  const builtinToolSchema = z.object({ type: z.string() }).loose();
@@ -135,11 +135,10 @@ export type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
135
135
 
136
136
  export type PreviousResponseReplayFailure = {
137
137
  code: "previous_response_not_found";
138
- reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
138
+ reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large" | "scope_mismatch";
139
139
  };
140
140
 
141
141
  const states = new Map<string, StoredResponseState>();
142
- const replayScopeMismatches = new WeakSet<object>();
143
142
  let storedResponseBytes = 0;
144
143
  let residentResponseBytes = 0;
145
144
  let oldestResidentId: string | undefined;
@@ -1044,11 +1043,6 @@ function normalizedClientThreadId(value: unknown): string | undefined {
1044
1043
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1045
1044
  }
1046
1045
 
1047
- function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
1048
- const { previous_response_id: _previousResponseId, ...freshRequest } = request;
1049
- return freshRequest;
1050
- }
1051
-
1052
1046
  export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
1053
1047
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
1054
1048
  const request = body as Record<string, unknown>;
@@ -1068,10 +1062,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1068
1062
  // A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
1069
1063
  // Unscoped callers retain backward-compatible replay only with other unscoped entries.
1070
1064
  if (requestThreadId !== storedThreadId) {
1071
- const freshRequest = withoutPreviousResponseId(request);
1072
- replayScopeMismatches.add(freshRequest);
1065
+ replayFailures.set(request, { code: "previous_response_not_found", reason: "scope_mismatch" });
1073
1066
  replayScopeMismatchDrops += 1;
1074
- return freshRequest;
1067
+ return body;
1075
1068
  }
1076
1069
  // The client already replayed this history verbatim. Prepending the stored copy would
1077
1070
  // double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
@@ -1136,9 +1129,9 @@ export function copyPreviousResponseReplayProvenance(source: unknown, target: un
1136
1129
  replayedInputPrefixLengths.set(target, prefixLength);
1137
1130
  }
1138
1131
 
1139
- /** True when a stale or foreign previous_response_id was removed from this exact request body. */
1132
+ /** True when this exact request could not replay because its task scope did not match. */
1140
1133
  export function previousResponseScopeMismatch(body: unknown): boolean {
1141
- return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
1134
+ return previousResponseReplayFailure(body)?.reason === "scope_mismatch";
1142
1135
  }
1143
1136
 
1144
1137
  export function previousResponseConversationId(responseId: string | undefined): string | undefined {
@@ -19,6 +19,21 @@ export function dottedAliasIsUnambiguous(namespace: string, name: string): boole
19
19
  return !namespace.includes("__") && !name.includes("__");
20
20
  }
21
21
 
22
+ /**
23
+ * Charset the Responses API enforces on a tool-call `name`.
24
+ *
25
+ * A name outside it is not merely rejected for the turn that carries it: Codex stores the item in
26
+ * the conversation, and every later request that replays that history is refused before it runs
27
+ * (`Invalid 'input[N].name': string does not match pattern '^[a-zA-Z0-9_-]+$'`). One relayed item
28
+ * therefore ends the conversation permanently, which is why the emit boundary treats an invalid
29
+ * name as a defect to repair rather than as a name to forward. See #5095.
30
+ */
31
+ const RESPONSES_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
32
+
33
+ export function isSchemaValidResponsesToolName(name: string): boolean {
34
+ return RESPONSES_TOOL_NAME_PATTERN.test(name);
35
+ }
36
+
22
37
  export function wireToolInnerName(tool: unknown): string | undefined {
23
38
  if (!isPlainObject(tool)) return undefined;
24
39
  const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
@@ -76,4 +91,3 @@ export function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): S
76
91
  for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
77
92
  return ambiguous;
78
93
  }
79
-
package/src/router.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
2
+ import { createHash } from "node:crypto";
3
+ import { peekAuthStore } from "./oauth/store";
2
4
  import {
3
5
  getCombo,
4
6
  isComboTargetInCooldown,
@@ -15,10 +17,12 @@ import { assertProviderDestinationAllowed } from "./lib/destination-policy";
15
17
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
16
18
  import {
17
19
  PROVIDER_REGISTRY,
18
- mergeRegistryStaticHeaders,
19
20
  providerCodexAccountMode,
20
- registryModelServiceTierCapabilityApplies,
21
+ registryEntryForProviderDestination,
21
22
  } from "./providers/registry";
23
+ // Imported from the module directly rather than through the registry facade, which is at its
24
+ // file-size cap.
25
+ import { registryModelIdKeys } from "./providers/registry/model-ids";
22
26
  import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
23
27
  import { cloneFastWire } from "./providers/fastwire";
24
28
  import {
@@ -29,13 +33,12 @@ import {
29
33
  isCanonicalOpenAiForwardProvider,
30
34
  LEGACY_CHATGPT_PROVIDER_ID,
31
35
  LEGACY_OPENAI_MULTI_PROVIDER_ID,
32
- OPENAI_API_PROVIDER_ID,
33
36
  OPENAI_CODEX_PROVIDER_ID,
34
37
  } from "./providers/openai-tiers";
35
38
  import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
36
- import { resolveModelAlias } from "./providers/default-aliases";
39
+ import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases";
37
40
  import { resolveBlockedModelRedirect } from "./lib/shadow-call";
38
- import { getStaleCached } from "./codex/model-cache";
41
+ import { getRoutingCached } from "./codex/model-cache";
39
42
  import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
40
43
  import {
41
44
  buildRouteDecisionTrace,
@@ -46,6 +49,7 @@ import {
46
49
  import { getRoutingProfile, resolvePolicyProfileId, POLICY_NAMESPACE } from "./routing/profile";
47
50
  import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
48
51
  import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
52
+ import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
49
53
 
50
54
  export class UnknownRoutingPolicyError extends Error {
51
55
  constructor(readonly profileId: string) {
@@ -69,6 +73,8 @@ export interface RouteResult {
69
73
  providerName: string;
70
74
  provider: OcxProviderConfig;
71
75
  modelId: string;
76
+ /** Immutable static policy for the current final wire model. */
77
+ staticPolicy: ResolvedModelPolicy;
72
78
  /** Which deterministic routing path produced this route (RI-01). */
73
79
  routeKind: RouteDecisionKind;
74
80
  /** Stable wire reason code for the selected route (RI-01). */
@@ -83,6 +89,29 @@ export interface RouteResult {
83
89
  routeDecision?: RouteDecisionTraceV1;
84
90
  }
85
91
 
92
+ export function captureRouteStaticPolicy(
93
+ providerName: string,
94
+ modelId: string,
95
+ provider: OcxProviderConfig,
96
+ effectiveAlias?: string | null,
97
+ inboundWire: "responses" | "chat" | "anthropic" = "responses",
98
+ ): ResolvedModelPolicy {
99
+ const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
100
+ const transportMatchedRegistry = !!registryEntry
101
+ && providerMatchesRegistryTransportWithStaticGuards(providerName, provider);
102
+ return resolveModelPolicy({
103
+ providerName,
104
+ modelId,
105
+ provider,
106
+ registryEntry,
107
+ transportMatchedRegistry,
108
+ inboundWire,
109
+ modelCapabilities: provider.modelCapabilities?.[modelId],
110
+ ...(provider.authMode ? { effectiveAuth: { authMode: provider.authMode } } : {}),
111
+ ...(effectiveAlias !== undefined ? { effectiveAlias } : {}),
112
+ });
113
+ }
114
+
86
115
  const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
87
116
  {
88
117
  providerNames: ["anthropic"],
@@ -117,86 +146,31 @@ export function knownModelIdsForProvider(
117
146
  : undefined;
118
147
  for (const id of registry?.models ?? []) ids.add(id);
119
148
  // Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
120
- // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
121
- for (const map of [
122
- registry?.modelContextWindows,
123
- registry?.modelInputModalities,
124
- registry?.modelReasoningEfforts,
125
- registry?.modelDefaultReasoningEfforts,
126
- registry?.modelReasoningEffortMap,
127
- registry?.modelMaxOutputTokens,
128
- registry?.modelSupportsServiceTier,
129
- registry?.modelSupportsVerbosity,
130
- ]) {
131
- for (const id of Object.keys(map ?? {})) ids.add(id);
132
- }
133
- for (const cached of getStaleCached(provName) ?? []) ids.add(cached.id);
149
+ // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps). Which
150
+ // maps count is classified by the registry itself rather than listed here, so an id declared
151
+ // only in a map this function forgot is no longer undecodable, and a new model-keyed field
152
+ // fails typecheck until its keys are given a meaning.
153
+ for (const id of registry ? registryModelIdKeys(registry) : []) ids.add(id);
154
+ const cachedModels = getRoutingCached(provName, () => {
155
+ // This callback runs only for a scoped entry, not for each provider in an alias scan.
156
+ const routed = routedProviderConfig(provName, prov);
157
+ let key = routed.apiKey;
158
+ if (routed.authMode === "oauth") {
159
+ const set = peekAuthStore()[provName];
160
+ const account = set?.accounts.find(row => row.id === set.activeAccountId);
161
+ if (!account || account.needsReauth || !Number.isFinite(account.credential.expires)
162
+ || account.credential.expires <= Date.now()) return undefined;
163
+ key = account.credential.access;
164
+ }
165
+ return key ? createHash("sha256").update(key).digest("hex") : undefined;
166
+ });
167
+ for (const cached of cachedModels ?? []) ids.add(cached.id);
134
168
  for (const model of config?.customModels ?? []) {
135
169
  if (model.provider === provName && model.modelId) ids.add(model.modelId);
136
170
  }
137
171
  return [...ids];
138
172
  }
139
173
 
140
- // Merge registry-default effort maps under user values so built-in provider configs can
141
- // carry real upstream aliases without a disk migration. User overrides win per-key.
142
- function mergeRecord(
143
- seed: Record<string, string> | undefined,
144
- user: Record<string, string> | undefined,
145
- ): Record<string, string> | undefined {
146
- if (!seed && !user) return undefined;
147
- return { ...(seed ?? {}), ...(user ?? {}) };
148
- }
149
-
150
- function mergeNestedRecord(
151
- seed: Record<string, Record<string, string>> | undefined,
152
- user: Record<string, Record<string, string>> | undefined,
153
- ): Record<string, Record<string, string>> | undefined {
154
- if (!seed && !user) return undefined;
155
- const out: Record<string, Record<string, string>> = {};
156
- for (const [key, value] of Object.entries(seed ?? {})) out[key] = { ...value };
157
- for (const [key, value] of Object.entries(user ?? {})) out[key] = { ...(out[key] ?? {}), ...value };
158
- return out;
159
- }
160
-
161
- function mergeStringArray(
162
- seed: string[] | undefined,
163
- user: string[] | undefined,
164
- ): string[] | undefined {
165
- if (!seed && !user) return undefined;
166
- return [...new Set([...(seed ?? []), ...(user ?? [])])];
167
- }
168
-
169
- function mergeRecordFill<T>(
170
- seed: Record<string, T> | undefined,
171
- user: Record<string, T> | undefined,
172
- ): Record<string, T> | undefined {
173
- if (!seed && !user) return undefined;
174
- return { ...(seed ?? {}), ...(user ?? {}) };
175
- }
176
-
177
- function mergePositiveNumberCaps(
178
- seed: Record<string, number> | undefined,
179
- user: Record<string, number> | undefined,
180
- ): Record<string, number> | undefined {
181
- if (!seed && !user) return undefined;
182
- const out = { ...(seed ?? {}) };
183
- for (const [key, value] of Object.entries(user ?? {})) {
184
- out[key] = typeof out[key] === "number" ? Math.min(out[key]!, value) : value;
185
- }
186
- return out;
187
- }
188
-
189
- function mergeStringArrayRecord(
190
- seed: Record<string, string[]> | undefined,
191
- user: Record<string, string[]> | undefined,
192
- ): Record<string, string[]> | undefined {
193
- if (!seed && !user) return undefined;
194
- const out: Record<string, string[]> = {};
195
- for (const [key, value] of Object.entries(seed ?? {})) out[key] = [...value];
196
- for (const [key, value] of Object.entries(user ?? {})) out[key] = [...value];
197
- return out;
198
- }
199
-
200
174
  /** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */
201
175
  function isSameEndpoint(a: string, b: string): boolean {
202
176
  return a.trim().replace(/\/+$/, "") === b.trim().replace(/\/+$/, "");
@@ -302,7 +276,20 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
302
276
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
303
277
  if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
304
278
  assertProviderDestinationAllowed(providerName, provider);
305
- return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
279
+ // A row whose adapter no longer matches its registry entry still reaches the Responses
280
+ // adapter when one model opts in through `modelAdapters` — a Volcengine Coding Plan config
281
+ // saved on Chat, for instance. The replay-drop flag belongs to the DESTINATION rather than
282
+ // to the provider-wide wire, so it is filled here too; without it that continuation forwards
283
+ // the reasoning item the upstream answers 400 to. Destination matching refuses templated and
284
+ // overridable base URLs, so this cannot follow a retargeted row, and an explicit value wins.
285
+ const destination = registryEntryForProviderDestination(provider);
286
+ return {
287
+ ...provider,
288
+ apiKey: usableResolvedApiKey(provider.apiKey),
289
+ ...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined
290
+ ? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems }
291
+ : {}),
292
+ };
306
293
  }
307
294
  const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
308
295
  const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
@@ -317,46 +304,42 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
317
304
  : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
318
305
  ? registryEntry.authKind
319
306
  : provider.authMode === "forward" ? undefined : provider.authMode;
320
- const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
321
- const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
322
- const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
323
- const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
324
- const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
325
- ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
326
- : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
327
- const modelInputModalities = mergeRecordFill(registryEntry.modelInputModalities, provider.modelInputModalities);
307
+ const staticPolicy = resolveModelPolicy({
308
+ providerName,
309
+ modelId: "__provider_static__",
310
+ provider,
311
+ registryEntry,
312
+ transportMatchedRegistry: true,
313
+ ...(canonicalAuthMode ? { effectiveAuth: { authMode: canonicalAuthMode } } : {}),
314
+ }).provider;
315
+ const reasoningEffortMap = staticPolicy.reasoningEffortMap;
316
+ const modelReasoningEffortMap = staticPolicy.modelReasoningEffortMap;
317
+ const modelReasoningEfforts = staticPolicy.modelReasoningEfforts;
318
+ const modelDefaultReasoningEfforts = staticPolicy.modelDefaultReasoningEfforts;
319
+ const modelContextWindows = staticPolicy.modelContextWindows;
320
+ const modelInputModalities = staticPolicy.modelInputModalities;
328
321
  // Registry static headers are documented as applying to every upstream request, so they are
329
322
  // filled at resolve time rather than only at seed time: a config written before a header
330
323
  // existed, or one carrying any header of its own, would otherwise never receive it. User
331
324
  // headers win, matched case-insensitively so an override replaces rather than duplicates.
332
- const headers = mergeRegistryStaticHeaders(registryEntry.staticHeaders, provider.headers);
333
- const modelMaxInputTokens = providerName === OPENAI_API_PROVIDER_ID
334
- ? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
335
- : mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
336
- const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
337
- const modelSupportsServiceTier = mergeRecordFill(
338
- registryModelServiceTierCapabilityApplies(registryEntry, provider)
339
- ? registryEntry.modelSupportsServiceTier
340
- : undefined,
341
- provider.modelSupportsServiceTier,
342
- );
343
- const modelSupportsVerbosity = mergeRecordFill(
344
- registryEntry.modelSupportsVerbosity,
345
- provider.modelSupportsVerbosity,
346
- );
347
- const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
348
- const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
349
- const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
350
- const noTopPModels = mergeStringArray(registryEntry.noTopPModels, provider.noTopPModels);
351
- const noPenaltyModels = mergeStringArray(registryEntry.noPenaltyModels, provider.noPenaltyModels);
352
- const noJsonSchemaModels = mergeStringArray(registryEntry.noJsonSchemaModels, provider.noJsonSchemaModels);
353
- const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
354
- const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
355
- const requiresReasoningPlaceholderModels = mergeStringArray(registryEntry.requiresReasoningPlaceholderModels, provider.requiresReasoningPlaceholderModels);
356
- const reasoningSplitModels = mergeStringArray(registryEntry.reasoningSplitModels, provider.reasoningSplitModels);
357
- const reasoningDetailsModels = mergeStringArray(registryEntry.reasoningDetailsModels, provider.reasoningDetailsModels);
358
- const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
359
- const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
325
+ const headers = staticPolicy.headers;
326
+ const modelMaxInputTokens = staticPolicy.modelMaxInputTokens;
327
+ const modelMaxOutputTokens = staticPolicy.modelMaxOutputTokens;
328
+ const modelSupportsServiceTier = staticPolicy.modelSupportsServiceTier;
329
+ const modelSupportsVerbosity = staticPolicy.modelSupportsVerbosity;
330
+ const noVisionModels = staticPolicy.noVisionModels;
331
+ const noReasoningModels = staticPolicy.noReasoningModels;
332
+ const noTemperatureModels = staticPolicy.noTemperatureModels;
333
+ const noTopPModels = staticPolicy.noTopPModels;
334
+ const noPenaltyModels = staticPolicy.noPenaltyModels;
335
+ const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
336
+ const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
337
+ const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels;
338
+ const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels;
339
+ const reasoningSplitModels = staticPolicy.reasoningSplitModels;
340
+ const reasoningDetailsModels = staticPolicy.reasoningDetailsModels;
341
+ const thinkingToggleModels = staticPolicy.thinkingToggleModels;
342
+ const thinkingBudgetModels = staticPolicy.thinkingBudgetModels;
360
343
  const registryBaseUrlIsTemplate = /\{[^}]*\}/.test(registryEntry.baseUrl);
361
344
  const userBaseUrl = typeof provider.baseUrl === "string" ? provider.baseUrl.trim() : "";
362
345
  const userBaseUrlIsResolved = userBaseUrl.length > 0 && !/\{[^}]*\}/.test(userBaseUrl);
@@ -417,6 +400,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
417
400
  ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
418
401
  ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
419
402
  : {}),
403
+ ...(provider.dropResponsesReasoningItems === undefined && registryEntry.dropResponsesReasoningItems !== undefined
404
+ ? { dropResponsesReasoningItems: registryEntry.dropResponsesReasoningItems }
405
+ : {}),
420
406
  // The request path resolves through routedProviderConfig() and never calls
421
407
  // enrichProviderFromRegistry(), so a saved provider row written before the
422
408
  // registry learned this flag must be backfilled here or route.provider never
@@ -565,10 +551,15 @@ function routeResult(
565
551
  const effectiveModelId = redirected ?? modelId;
566
552
  const effectiveRouteReason = redirected ? "blocked-model-redirect" : routeReason;
567
553
  const codexAccountMode = providerCodexAccountMode(providerName, provider);
554
+ const routedProvider = routedProviderConfig(providerName, provider);
555
+ const effectiveAlias = config
556
+ ? effectiveProviderAliasDecision(providerName, provider, config)
557
+ : undefined;
568
558
  return {
569
559
  providerName,
570
- provider: routedProviderConfig(providerName, provider),
560
+ provider: routedProvider,
571
561
  modelId: effectiveModelId,
562
+ staticPolicy: captureRouteStaticPolicy(providerName, effectiveModelId, routedProvider, effectiveAlias),
572
563
  routeKind,
573
564
  routeReason: effectiveRouteReason,
574
565
  ...(codexAccountMode ? { codexAccountMode } : {}),
@@ -200,6 +200,12 @@ export function resolveProductionBehaviorValues(
200
200
  : reasoningEfforts.length > 0 || Boolean(registryEntry?.reasoningEfforts),
201
201
  ),
202
202
  "reasoning.efforts": behaviorRow("provider_config", reasoningEfforts),
203
+ // Whether the configured ladder above is what the WIRE uses, or only what the catalog
204
+ // advertises. An adapter shipping its own per-model table (command-code) otherwise wins for
205
+ // models it knows, and it also repairs a refused rung by replaying without the effort. Two
206
+ // routes that disagree about this send different bytes and recover differently, so they are
207
+ // not the same compatibility subject and must not share evidence.
208
+ "reasoning.effortsAuthoritative": behaviorRow("provider_config", effective.modelReasoningEffortsAuthoritative === true),
203
209
  "reasoning.defaultEffort": behaviorRow("provider_config", defaultReasoningEffort),
204
210
  "reasoning.effortMap": behaviorRow("provider_config", reasoningEffortMap),
205
211
  "reasoning.wireFormat": behaviorRow("provider_config", effective.reasoningWireFormat ?? null),
@@ -209,6 +215,9 @@ export function resolveProductionBehaviorValues(
209
215
  }),
210
216
  "reasoning.replayMode": behaviorRow("provider_config", {
211
217
  preserveResponses: effective.preserveResponsesReasoningContent === true,
218
+ // Dropping replayed reasoning items changes the continuation body, so two routes that
219
+ // disagree about it are not the same compatibility subject and must not share evidence.
220
+ dropResponses: effective.dropResponsesReasoningItems === true,
212
221
  preserveContent: includesModel(effective.preserveReasoningContentModels, modelId),
213
222
  placeholder: includesModel(effective.requiresReasoningPlaceholderModels, modelId),
214
223
  }),
@@ -1,5 +1,8 @@
1
1
  import type { OcxConfig, OcxProviderConfig } from "../../types";
2
2
  import type { InboundWire } from "../../providers/registry";
3
+ import { getProviderRegistryEntry } from "../../providers/registry";
4
+ import { providerMatchesRegistryTransportWithStaticGuards } from "../../providers/static-model-discovery";
5
+ import { resolveModelPolicy } from "../../providers/resolved-model-policy";
3
6
  import { subjectIdForSubject } from "../../lab/digest";
4
7
  import { buildRouteSubjectV1 } from "../../lab/subject/route-subject";
5
8
  import { buildProtocolSubjectV1 } from "../../lab/subject/protocol-subject";
@@ -65,7 +68,19 @@ export function resolveCompatibilitySubjectsForInboundWire(
65
68
  inboundWire: InboundWire,
66
69
  configDir?: string,
67
70
  ): ResolvedPolicyCompatibilitySubjects {
68
- const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire);
71
+ const registryEntry = getProviderRegistryEntry(providerName);
72
+ const staticPolicy = resolveModelPolicy({
73
+ providerName,
74
+ modelId,
75
+ provider: routed,
76
+ registryEntry,
77
+ transportMatchedRegistry: !!registryEntry
78
+ && providerMatchesRegistryTransportWithStaticGuards(providerName, routed),
79
+ inboundWire,
80
+ modelCapabilities: routed.modelCapabilities?.[modelId],
81
+ ...(routed.authMode ? { effectiveAuth: { authMode: routed.authMode } } : {}),
82
+ });
83
+ const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire, staticPolicy);
69
84
  const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
70
85
  const adapter = effective.adapter ?? "openai-responses";
71
86
  const inboundProtocol = inboundProtocolForWire(inboundWire);