@mupt-ai/dari-gateway 0.1.0 → 0.2.1

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 (369) hide show
  1. package/README.md +8 -1
  2. package/dist/bun.d.ts +2 -0
  3. package/dist/bun.js +4 -0
  4. package/dist/context/eval_catalog.d.ts +3 -0
  5. package/dist/context/eval_catalog.js +7 -0
  6. package/dist/context/interaction_fingerprint.d.ts +27 -0
  7. package/dist/context/interaction_fingerprint.js +519 -0
  8. package/dist/context/interaction_fingerprint_format.d.ts +42 -0
  9. package/dist/context/interaction_fingerprint_format.js +113 -0
  10. package/dist/context/interaction_lease_depth.d.ts +2 -0
  11. package/dist/context/interaction_lease_depth.js +110 -0
  12. package/dist/context/lease_history.d.ts +7 -0
  13. package/dist/context/lease_history.js +158 -0
  14. package/dist/context/pricing.d.ts +4 -0
  15. package/dist/context/pricing.js +23 -0
  16. package/dist/context/provider_prompt_accounting.d.ts +14 -0
  17. package/dist/context/provider_prompt_accounting.js +187 -0
  18. package/dist/context/routing_context.d.ts +40 -0
  19. package/dist/context/routing_context.js +211 -0
  20. package/dist/http/auth.d.ts +2 -0
  21. package/dist/http/auth.js +35 -0
  22. package/dist/http/errors.d.ts +19 -0
  23. package/dist/http/errors.js +89 -0
  24. package/dist/http/handler.d.ts +12 -0
  25. package/dist/http/handler.js +81 -0
  26. package/dist/http/http2_server.d.ts +6 -0
  27. package/dist/http/http2_server.js +103 -0
  28. package/dist/http/serve.d.ts +20 -0
  29. package/dist/http/serve.js +78 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +8 -0
  32. package/dist/ingress/anthropic_messages.d.ts +3 -0
  33. package/dist/ingress/anthropic_messages.js +122 -0
  34. package/dist/ingress/anthropic_models.d.ts +1 -0
  35. package/dist/ingress/anthropic_models.js +23 -0
  36. package/dist/ingress/chat_completions.d.ts +2 -0
  37. package/dist/ingress/chat_completions.js +37 -0
  38. package/dist/ingress/claude_code/request_context.d.ts +2 -0
  39. package/dist/ingress/claude_code/request_context.js +31 -0
  40. package/dist/ingress/codex.d.ts +3 -0
  41. package/dist/ingress/codex.js +65 -0
  42. package/dist/ingress/codex_models.d.ts +2 -0
  43. package/dist/ingress/codex_models.js +89 -0
  44. package/dist/ingress/json.d.ts +1 -0
  45. package/dist/ingress/json.js +95 -0
  46. package/dist/ingress/models.d.ts +1 -0
  47. package/dist/ingress/models.js +53 -0
  48. package/dist/interaction/capabilities.d.ts +3 -0
  49. package/dist/interaction/capabilities.js +24 -0
  50. package/dist/interaction/image.d.ts +4 -0
  51. package/dist/interaction/image.js +13 -0
  52. package/dist/interaction/portable_reasoning_state.d.ts +4 -0
  53. package/dist/interaction/portable_reasoning_state.js +18 -0
  54. package/dist/interaction/provider_continuation_state.d.ts +6 -0
  55. package/dist/interaction/provider_continuation_state.js +27 -0
  56. package/dist/interaction/tool_identity.d.ts +10 -0
  57. package/dist/interaction/tool_identity.js +41 -0
  58. package/dist/interaction/types.d.ts +203 -0
  59. package/dist/interaction/types.js +5 -0
  60. package/dist/models/context_limits.d.ts +2 -0
  61. package/dist/models/context_limits.js +7 -0
  62. package/dist/models/execution_provider.d.ts +8 -0
  63. package/dist/models/execution_provider.js +13 -0
  64. package/dist/models/gateways.d.ts +11 -0
  65. package/dist/models/gateways.js +45 -0
  66. package/dist/models/ids.d.ts +1 -0
  67. package/dist/models/ids.js +15 -0
  68. package/dist/models/provider_defaults.d.ts +4 -0
  69. package/dist/models/provider_defaults.js +41 -0
  70. package/dist/models/runtime_overrides.d.ts +4 -0
  71. package/dist/models/runtime_overrides.js +209 -0
  72. package/dist/observability/errors.d.ts +8 -0
  73. package/dist/observability/errors.js +11 -0
  74. package/dist/platform/platform.d.ts +152 -0
  75. package/dist/platform/platform.js +11 -0
  76. package/dist/platform/routing_contract.d.ts +2 -0
  77. package/dist/platform/routing_contract.js +5 -0
  78. package/dist/protocol/input/anthropic_messages.d.ts +2 -0
  79. package/dist/protocol/input/anthropic_messages.js +494 -0
  80. package/dist/protocol/input/chat_completions.d.ts +4 -0
  81. package/dist/protocol/input/chat_completions.js +212 -0
  82. package/dist/protocol/input/chat_completions_validation.d.ts +2 -0
  83. package/dist/protocol/input/chat_completions_validation.js +257 -0
  84. package/dist/protocol/input/openai_responses.d.ts +2 -0
  85. package/dist/protocol/input/openai_responses.js +666 -0
  86. package/dist/protocol/input/router_model.d.ts +7 -0
  87. package/dist/protocol/input/router_model.js +43 -0
  88. package/dist/protocol/output/anthropic_messages.d.ts +10 -0
  89. package/dist/protocol/output/anthropic_messages.js +15 -0
  90. package/dist/protocol/output/anthropic_messages_output.d.ts +10 -0
  91. package/dist/protocol/output/anthropic_messages_output.js +124 -0
  92. package/dist/protocol/output/anthropic_messages_stream.d.ts +16 -0
  93. package/dist/protocol/output/anthropic_messages_stream.js +267 -0
  94. package/dist/protocol/output/chat_completions.d.ts +29 -0
  95. package/dist/protocol/output/chat_completions.js +406 -0
  96. package/dist/protocol/output/chat_reasoning_detail.d.ts +2 -0
  97. package/dist/protocol/output/chat_reasoning_detail.js +13 -0
  98. package/dist/protocol/output/delivered_stream_snapshot.d.ts +10 -0
  99. package/dist/protocol/output/delivered_stream_snapshot.js +31 -0
  100. package/dist/protocol/output/openai_responses.d.ts +10 -0
  101. package/dist/protocol/output/openai_responses.js +14 -0
  102. package/dist/protocol/output/openai_responses_output.d.ts +14 -0
  103. package/dist/protocol/output/openai_responses_output.js +125 -0
  104. package/dist/protocol/output/openai_responses_stream.d.ts +18 -0
  105. package/dist/protocol/output/openai_responses_stream.js +546 -0
  106. package/dist/protocol/output/sse_stream.d.ts +31 -0
  107. package/dist/protocol/output/sse_stream.js +67 -0
  108. package/dist/providers/anthropic_safeguards.d.ts +11 -0
  109. package/dist/providers/anthropic_safeguards.js +81 -0
  110. package/dist/providers/aws_assume_role.d.ts +17 -0
  111. package/dist/providers/aws_assume_role.js +107 -0
  112. package/dist/providers/cancel_estimate.d.ts +43 -0
  113. package/dist/providers/cancel_estimate.js +89 -0
  114. package/dist/providers/execution.d.ts +72 -0
  115. package/dist/providers/execution.js +491 -0
  116. package/dist/providers/model_capabilities.d.ts +11 -0
  117. package/dist/providers/model_capabilities.js +29 -0
  118. package/dist/providers/pi.d.ts +65 -0
  119. package/dist/providers/pi.js +319 -0
  120. package/dist/providers/pi_context.d.ts +7 -0
  121. package/dist/providers/pi_context.js +405 -0
  122. package/dist/providers/pi_options.d.ts +11 -0
  123. package/dist/providers/pi_options.js +398 -0
  124. package/dist/providers/pi_output.d.ts +2 -0
  125. package/dist/providers/pi_output.js +453 -0
  126. package/dist/providers/provider_attempt_runner.d.ts +55 -0
  127. package/dist/providers/provider_attempt_runner.js +90 -0
  128. package/dist/providers/provider_call.d.ts +77 -0
  129. package/dist/providers/provider_call.js +292 -0
  130. package/dist/providers/provider_reported_cost.d.ts +10 -0
  131. package/dist/providers/provider_reported_cost.js +123 -0
  132. package/dist/providers/reasoning.d.ts +12 -0
  133. package/dist/providers/reasoning.js +30 -0
  134. package/dist/providers/response_capture.d.ts +23 -0
  135. package/dist/providers/response_capture.js +47 -0
  136. package/dist/providers/stream_finalization.d.ts +46 -0
  137. package/dist/providers/stream_finalization.js +165 -0
  138. package/dist/providers/subscription_limits.d.ts +31 -0
  139. package/dist/providers/subscription_limits.js +163 -0
  140. package/dist/providers/subscription_usage.d.ts +10 -0
  141. package/dist/providers/subscription_usage.js +79 -0
  142. package/dist/runtime/claude_code/auxiliary_routing.d.ts +3 -0
  143. package/dist/runtime/claude_code/auxiliary_routing.js +60 -0
  144. package/dist/runtime/customer_disconnect.d.ts +20 -0
  145. package/dist/runtime/customer_disconnect.js +49 -0
  146. package/dist/runtime/explicit_model.d.ts +4 -0
  147. package/dist/runtime/explicit_model.js +53 -0
  148. package/dist/runtime/harness_identity/conversation.d.ts +33 -0
  149. package/dist/runtime/harness_identity/conversation.js +39 -0
  150. package/dist/runtime/harness_identity/extract.d.ts +8 -0
  151. package/dist/runtime/harness_identity/extract.js +214 -0
  152. package/dist/runtime/harness_identity/index.d.ts +2 -0
  153. package/dist/runtime/harness_identity/index.js +2 -0
  154. package/dist/runtime/prepare_routed_completion.d.ts +33 -0
  155. package/dist/runtime/prepare_routed_completion.js +167 -0
  156. package/dist/runtime/provider_stream_lifecycle.d.ts +31 -0
  157. package/dist/runtime/provider_stream_lifecycle.js +88 -0
  158. package/dist/runtime/provider_stream_outcome.d.ts +23 -0
  159. package/dist/runtime/provider_stream_outcome.js +55 -0
  160. package/dist/runtime/request_tags.d.ts +7 -0
  161. package/dist/runtime/request_tags.js +33 -0
  162. package/dist/runtime/routed_completion.d.ts +17 -0
  163. package/dist/runtime/routed_completion.js +80 -0
  164. package/dist/runtime/routed_protocol.d.ts +30 -0
  165. package/dist/runtime/routed_protocol.js +1 -0
  166. package/dist/runtime/routed_runtime.d.ts +40 -0
  167. package/dist/runtime/routed_runtime.js +535 -0
  168. package/dist/runtime/routing_lease.d.ts +26 -0
  169. package/dist/runtime/routing_lease.js +210 -0
  170. package/dist/runtime/runtime_reporter.d.ts +71 -0
  171. package/dist/runtime/runtime_reporter.js +35 -0
  172. package/dist/self_host/config.d.ts +46 -0
  173. package/dist/self_host/config.js +105 -0
  174. package/dist/self_host/main.d.ts +16 -0
  175. package/dist/self_host/main.js +95 -0
  176. package/dist/self_host/platform.d.ts +13 -0
  177. package/dist/self_host/platform.js +50 -0
  178. package/dist/self_host/prefix_store.d.ts +46 -0
  179. package/dist/self_host/prefix_store.js +261 -0
  180. package/dist/self_host/resolve.d.ts +28 -0
  181. package/dist/self_host/resolve.js +171 -0
  182. package/dist/self_host/subscriptions/access.d.ts +10 -0
  183. package/dist/self_host/subscriptions/access.js +37 -0
  184. package/dist/self_host/subscriptions/http.d.ts +15 -0
  185. package/dist/self_host/subscriptions/http.js +187 -0
  186. package/dist/self_host/subscriptions/index.d.ts +24 -0
  187. package/dist/self_host/subscriptions/index.js +24 -0
  188. package/dist/self_host/subscriptions/oauth.d.ts +55 -0
  189. package/dist/self_host/subscriptions/oauth.js +281 -0
  190. package/dist/self_host/subscriptions/store.d.ts +78 -0
  191. package/dist/self_host/subscriptions/store.js +235 -0
  192. package/dist/strategies/auto_routing_selector.d.ts +12 -0
  193. package/dist/strategies/auto_routing_selector.js +61 -0
  194. package/dist/strategies/eval_relevance.d.ts +17 -0
  195. package/dist/strategies/eval_relevance.js +32 -0
  196. package/dist/strategies/fallback_decision.d.ts +8 -0
  197. package/dist/strategies/fallback_decision.js +17 -0
  198. package/dist/strategies/priority_selector.d.ts +7 -0
  199. package/dist/strategies/priority_selector.js +37 -0
  200. package/dist/strategies/route_decision.d.ts +22 -0
  201. package/dist/strategies/route_decision.js +65 -0
  202. package/dist/strategies/routing.d.ts +74 -0
  203. package/dist/strategies/routing.js +280 -0
  204. package/dist/strategies/selector_projection.d.ts +15 -0
  205. package/dist/strategies/selector_projection.js +161 -0
  206. package/dist/telemetry/background.d.ts +7 -0
  207. package/dist/telemetry/background.js +33 -0
  208. package/dist/telemetry/benchmark_replay.d.ts +37 -0
  209. package/dist/telemetry/benchmark_replay.js +108 -0
  210. package/dist/telemetry/billing_finalization.d.ts +64 -0
  211. package/dist/telemetry/billing_finalization.js +96 -0
  212. package/dist/telemetry/ingest_values.d.ts +4 -0
  213. package/dist/telemetry/ingest_values.js +12 -0
  214. package/dist/telemetry/model_cost.d.ts +3 -0
  215. package/dist/telemetry/model_cost.js +15 -0
  216. package/dist/telemetry/prefix_state.d.ts +47 -0
  217. package/dist/telemetry/prefix_state.js +116 -0
  218. package/dist/telemetry/report.d.ts +88 -0
  219. package/dist/telemetry/report.js +621 -0
  220. package/dist/telemetry/requests/interaction_observability.d.ts +45 -0
  221. package/dist/telemetry/requests/interaction_observability.js +180 -0
  222. package/dist/telemetry/requests/retained_interaction_request.d.ts +8 -0
  223. package/dist/telemetry/requests/retained_interaction_request.js +10 -0
  224. package/dist/telemetry/scrub.d.ts +2 -0
  225. package/dist/telemetry/scrub.js +31 -0
  226. package/dist/telemetry/skills/claude/catalog.d.ts +6 -0
  227. package/dist/telemetry/skills/claude/catalog.js +99 -0
  228. package/dist/telemetry/skills/claude/file_reads.d.ts +4 -0
  229. package/dist/telemetry/skills/claude/file_reads.js +110 -0
  230. package/dist/telemetry/skills/claude/observations.d.ts +3 -0
  231. package/dist/telemetry/skills/claude/observations.js +366 -0
  232. package/dist/telemetry/skills/codex/catalog.d.ts +11 -0
  233. package/dist/telemetry/skills/codex/catalog.js +138 -0
  234. package/dist/telemetry/skills/codex/exec_file_reads.d.ts +9 -0
  235. package/dist/telemetry/skills/codex/exec_file_reads.js +36 -0
  236. package/dist/telemetry/skills/codex/explicit.d.ts +3 -0
  237. package/dist/telemetry/skills/codex/explicit.js +90 -0
  238. package/dist/telemetry/skills/codex/file_reads.d.ts +4 -0
  239. package/dist/telemetry/skills/codex/file_reads.js +37 -0
  240. package/dist/telemetry/skills/codex/identity.d.ts +2 -0
  241. package/dist/telemetry/skills/codex/identity.js +20 -0
  242. package/dist/telemetry/skills/observations.d.ts +21 -0
  243. package/dist/telemetry/skills/observations.js +41 -0
  244. package/dist/telemetry/skills/paths.d.ts +11 -0
  245. package/dist/telemetry/skills/paths.js +61 -0
  246. package/dist/telemetry/skills/pi.d.ts +6 -0
  247. package/dist/telemetry/skills/pi.js +223 -0
  248. package/dist/telemetry/skills/shell_file_reads.d.ts +7 -0
  249. package/dist/telemetry/skills/shell_file_reads.js +226 -0
  250. package/dist/telemetry/skills/shell_words.d.ts +13 -0
  251. package/dist/telemetry/skills/shell_words.js +189 -0
  252. package/dist/telemetry/tools/input.d.ts +2 -0
  253. package/dist/telemetry/tools/input.js +14 -0
  254. package/dist/telemetry/tools/observations.d.ts +29 -0
  255. package/dist/telemetry/tools/observations.js +247 -0
  256. package/dist/telemetry/usage.d.ts +57 -0
  257. package/dist/telemetry/usage.js +136 -0
  258. package/dist/types.d.ts +94 -0
  259. package/dist/types.js +1 -0
  260. package/package.json +22 -5
  261. package/src/bun.ts +4 -0
  262. package/src/context/eval_catalog.ts +1 -1
  263. package/src/context/interaction_fingerprint.ts +9 -9
  264. package/src/context/interaction_fingerprint_format.ts +1 -1
  265. package/src/context/interaction_lease_depth.ts +1 -1
  266. package/src/context/pricing.ts +1 -1
  267. package/src/context/provider_prompt_accounting.ts +5 -5
  268. package/src/context/routing_context.ts +17 -17
  269. package/src/http/auth.ts +1 -1
  270. package/src/http/handler.ts +8 -8
  271. package/src/http/serve.ts +3 -3
  272. package/src/index.ts +10 -9
  273. package/src/ingress/anthropic_messages.ts +12 -12
  274. package/src/ingress/anthropic_models.ts +1 -1
  275. package/src/ingress/chat_completions.ts +9 -9
  276. package/src/ingress/claude_code/request_context.ts +2 -2
  277. package/src/ingress/codex.ts +10 -10
  278. package/src/ingress/codex_models.ts +4 -4
  279. package/src/ingress/json.ts +1 -1
  280. package/src/ingress/models.ts +10 -10
  281. package/src/interaction/capabilities.ts +1 -1
  282. package/src/interaction/portable_reasoning_state.ts +1 -1
  283. package/src/interaction/provider_continuation_state.ts +1 -1
  284. package/src/interaction/tool_identity.ts +2 -2
  285. package/src/interaction/types.ts +3 -0
  286. package/src/models/context_limits.ts +1 -1
  287. package/src/models/execution_provider.ts +1 -1
  288. package/src/models/ids.ts +1 -1
  289. package/src/models/provider_defaults.ts +1 -1
  290. package/src/models/runtime_overrides.ts +1 -1
  291. package/src/observability/errors.ts +2 -2
  292. package/src/platform/platform.ts +36 -4
  293. package/src/protocol/input/anthropic_messages.ts +7 -7
  294. package/src/protocol/input/chat_completions.ts +5 -5
  295. package/src/protocol/input/chat_completions_validation.ts +4 -4
  296. package/src/protocol/input/openai_responses.ts +6 -6
  297. package/src/protocol/output/anthropic_messages.ts +3 -3
  298. package/src/protocol/output/anthropic_messages_output.ts +6 -4
  299. package/src/protocol/output/anthropic_messages_stream.ts +7 -7
  300. package/src/protocol/output/chat_completions.ts +13 -13
  301. package/src/protocol/output/chat_reasoning_detail.ts +1 -1
  302. package/src/protocol/output/openai_responses.ts +3 -3
  303. package/src/protocol/output/openai_responses_output.ts +4 -2
  304. package/src/protocol/output/openai_responses_stream.ts +6 -6
  305. package/src/protocol/output/sse_stream.ts +3 -3
  306. package/src/providers/anthropic_safeguards.ts +2 -2
  307. package/src/providers/aws_assume_role.ts +2 -2
  308. package/src/providers/cancel_estimate.ts +1 -1
  309. package/src/providers/execution.ts +17 -17
  310. package/src/providers/pi.ts +115 -41
  311. package/src/providers/pi_context.ts +6 -6
  312. package/src/providers/pi_options.ts +283 -453
  313. package/src/providers/pi_output.ts +6 -5
  314. package/src/providers/provider_attempt_runner.ts +1 -1
  315. package/src/providers/provider_call.ts +37 -18
  316. package/src/providers/provider_reported_cost.ts +138 -0
  317. package/src/providers/reasoning.ts +2 -2
  318. package/src/providers/stream_finalization.ts +10 -10
  319. package/src/providers/subscription_limits.ts +3 -3
  320. package/src/runtime/claude_code/auxiliary_routing.ts +5 -5
  321. package/src/runtime/customer_disconnect.ts +1 -1
  322. package/src/runtime/explicit_model.ts +31 -4
  323. package/src/runtime/harness_identity/conversation.ts +1 -1
  324. package/src/runtime/harness_identity/extract.ts +3 -3
  325. package/src/runtime/harness_identity/index.ts +2 -2
  326. package/src/runtime/prepare_routed_completion.ts +46 -10
  327. package/src/runtime/provider_stream_lifecycle.ts +1 -1
  328. package/src/runtime/provider_stream_outcome.ts +2 -2
  329. package/src/runtime/request_tags.ts +34 -0
  330. package/src/runtime/routed_completion.ts +13 -11
  331. package/src/runtime/routed_protocol.ts +2 -2
  332. package/src/runtime/routed_runtime.ts +38 -20
  333. package/src/runtime/routing_lease.ts +7 -7
  334. package/src/runtime/runtime_reporter.ts +3 -3
  335. package/src/self_host/main.ts +19 -14
  336. package/src/self_host/platform.ts +5 -5
  337. package/src/self_host/prefix_store.ts +2 -2
  338. package/src/self_host/resolve.ts +11 -11
  339. package/src/self_host/subscriptions/access.ts +2 -2
  340. package/src/self_host/subscriptions/http.ts +2 -2
  341. package/src/self_host/subscriptions/index.ts +4 -4
  342. package/src/self_host/subscriptions/store.ts +1 -1
  343. package/src/strategies/auto_routing_selector.ts +2 -2
  344. package/src/strategies/eval_relevance.ts +3 -3
  345. package/src/strategies/priority_selector.ts +1 -1
  346. package/src/strategies/route_decision.ts +7 -7
  347. package/src/strategies/routing.ts +11 -11
  348. package/src/strategies/selector_projection.ts +2 -2
  349. package/src/telemetry/billing_finalization.ts +3 -3
  350. package/src/telemetry/model_cost.ts +1 -1
  351. package/src/telemetry/prefix_state.ts +4 -4
  352. package/src/telemetry/report.ts +30 -25
  353. package/src/telemetry/requests/interaction_observability.ts +1 -1
  354. package/src/telemetry/requests/retained_interaction_request.ts +1 -1
  355. package/src/telemetry/skills/claude/catalog.ts +1 -1
  356. package/src/telemetry/skills/claude/file_reads.ts +6 -6
  357. package/src/telemetry/skills/claude/observations.ts +3 -3
  358. package/src/telemetry/skills/codex/catalog.ts +3 -3
  359. package/src/telemetry/skills/codex/exec_file_reads.ts +4 -4
  360. package/src/telemetry/skills/codex/explicit.ts +3 -3
  361. package/src/telemetry/skills/codex/file_reads.ts +5 -5
  362. package/src/telemetry/skills/observations.ts +7 -7
  363. package/src/telemetry/skills/pi.ts +3 -3
  364. package/src/telemetry/skills/shell_file_reads.ts +2 -2
  365. package/src/telemetry/skills/shell_words.ts +1 -1
  366. package/src/telemetry/tools/input.ts +1 -1
  367. package/src/telemetry/tools/observations.ts +9 -9
  368. package/src/telemetry/usage.ts +9 -5
  369. package/src/types.ts +15 -0
@@ -1,30 +1,45 @@
1
1
  import type { BedrockOptions } from "@mupt-ai/pi-ai/api/bedrock-converse-stream";
2
2
  import { isReasoningEffort, isRecord, type ReasoningEffort } from "@mupt-ai/dari-router/policy-engine";
3
- import type { InteractionRequest, InteractionResponseFormat, InteractionToolChoice } from "../interaction/types";
4
- import { requiredCapabilitiesFor } from "../interaction/capabilities";
5
- import { toolWireName, toolsByWireName } from "../interaction/tool_identity";
6
- import { gatewayFor, payloadWithGatewayFields } from "../models/gateways";
7
- import { offRequiresWireNone } from "../models/runtime_overrides";
8
- import { effectiveReasoningForModel, validateReasoningEffortForModel } from "./reasoning";
9
- import { isBedrockApi, isOpenAIResponsesApi } from "./model_capabilities";
10
- import type { ResolvedProviderAuth } from "../types";
11
- import type { ProviderFetch } from "./response_capture";
12
- import { forwardsAnthropicSafeguards, payloadWithSafeguards } from "./anthropic_safeguards";
3
+ import type { InteractionRequest, InteractionResponseFormat, InteractionToolChoice } from "../interaction/types.js";
4
+ import { requiredCapabilitiesFor } from "../interaction/capabilities.js";
5
+ import { toolWireName, toolsByWireName } from "../interaction/tool_identity.js";
6
+ import { gatewayFor, payloadWithGatewayFields } from "../models/gateways.js";
7
+ import { offRequiresWireNone } from "../models/runtime_overrides.js";
8
+ import { effectiveReasoningForModel, validateReasoningEffortForModel } from "./reasoning.js";
9
+ import { isBedrockApi, isOpenAIResponsesApi } from "./model_capabilities.js";
10
+ import type { ResolvedProviderAuth } from "../types.js";
11
+ import type { ProviderFetch } from "./response_capture.js";
12
+ import { forwardsAnthropicSafeguards, payloadWithSafeguards } from "./anthropic_safeguards.js";
13
13
 
14
14
  type PiOptionsSupport = {
15
15
  getSupportedThinkingLevels?: (model: Record<string, unknown>) => string[];
16
16
  };
17
17
 
18
+ type Payload = Record<string, unknown>;
19
+ // A patch returns the next payload, or undefined to leave it unchanged.
20
+ type PayloadPatch = (payload: Payload) => unknown;
21
+ type Model = Record<string, unknown>;
22
+ type ImageDetail = "auto" | "low" | "high" | "original";
23
+
18
24
  const ANTHROPIC_FAST_MODE_BETA = "fast-mode-2026-02-01";
25
+ const ANTHROPIC_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
19
26
  // Anthropic rejects newer models from older Claude Code versions, so forward
20
27
  // the client's own user agent and keep this fallback current for other clients.
21
28
  const CLAUDE_CODE_USER_AGENT = "claude-cli/2.1.280";
29
+ const IMAGE_DETAILS: readonly unknown[] = ["auto", "low", "high", "original"];
30
+
31
+ const isAnthropic = (api: unknown) => api === "anthropic-messages";
32
+ const isCompletions = (api: unknown) => api === "openai-completions";
33
+ // Responses payloads Pi builds from the public OpenAI/Azure APIs (not Codex).
34
+ const isResponses = (api: unknown) => api === "openai-responses" || api === "azure-openai-responses";
35
+ const isOpenAI = (api: unknown) => isResponses(api) || isCompletions(api);
36
+ const isGoogle = (api: unknown) => api === "google-generative-ai" || api === "google-vertex";
22
37
 
23
38
  export function buildOptions(
24
39
  request: InteractionRequest,
25
40
  auth: ResolvedProviderAuth | string,
26
41
  selectedModel: string,
27
- model: Record<string, unknown>,
42
+ model: Model,
28
43
  pi: PiOptionsSupport,
29
44
  sessionId?: string,
30
45
  selectedReasoningEffort?: ReasoningEffort,
@@ -32,218 +47,153 @@ export function buildOptions(
32
47
  signal?: AbortSignal,
33
48
  fetch?: ProviderFetch,
34
49
  ): Record<string, unknown> {
35
- const providerAuth = typeof auth === "string" ? { type: "bearer" as const, apiKey: auth } : auth;
36
- const authentication = isBedrockApi(model.api)
37
- ? bedrockAuthenticationOptions(providerAuth)
38
- : providerAuthenticationOptions(providerAuth);
50
+ const api = model.api;
51
+ const providerAuth: ResolvedProviderAuth = typeof auth === "string" ? { type: "bearer", apiKey: auth } : auth;
52
+ const { generation, toolChoice, parallelToolCalls, responseFormat } = request;
53
+ const headers: Record<string, string> = {};
39
54
  const options: Record<string, unknown> = {
40
- ...authentication,
41
- ...(isClaudeCodeOAuth(providerAuth, model) ? { headers: { "user-agent": request.claudeCodeUserAgent ?? CLAUDE_CODE_USER_AGENT } } : {}),
55
+ ...authenticationOptions(providerAuth, api),
42
56
  maxRetries: 0,
43
57
  timeoutMs,
44
- ...(signal === undefined ? {} : { signal }),
45
- ...(fetch === undefined ? {} : { fetch }),
46
- metadata: {
47
- ...(request.metadata ?? {}),
48
- ...(request.user ? { user: request.user } : {}),
49
- },
58
+ signal,
59
+ fetch,
60
+ metadata: { ...request.metadata, ...(request.user ? { user: request.user } : {}) },
50
61
  // Feeds OpenAI prompt_cache_key + session-affinity headers inside pi-ai;
51
62
  // ignored by providers without session-based caching.
52
- ...(sessionId ? { sessionId } : {}),
63
+ sessionId: sessionId || undefined,
53
64
  };
65
+ const patches: PayloadPatch[] = [];
66
+
67
+ if (isClaudeCodeOAuth(providerAuth, api)) {
68
+ headers["user-agent"] = request.claudeCodeUserAgent ?? CLAUDE_CODE_USER_AGENT;
69
+ }
54
70
  // Pi's Anthropic transport honors compat.supportsTemperature; the OpenAI
55
71
  // transports forward temperature regardless, so drop it here for models
56
72
  // that reject it instead of failing the whole request.
57
- if (
58
- request.generation.temperature !== undefined
59
- && !(isRecord(model.compat) && model.compat.supportsTemperature === false)
60
- ) {
61
- options.temperature = request.generation.temperature;
62
- }
63
- if (request.generation.maxOutputTokens !== undefined) options.maxTokens = request.generation.maxOutputTokens;
64
- const payloadPatches: Array<(payload: Record<string, unknown>) => unknown> = [];
65
- if (request.generation.topP !== undefined || request.generation.stop !== undefined) {
66
- payloadPatches.push((payload) => payloadWithSamplingControls(payload, request, model));
73
+ if (!(isRecord(model.compat) && model.compat.supportsTemperature === false)) {
74
+ options.temperature = generation.temperature;
67
75
  }
68
- const requestedReasoningBudget = request.reasoning?.budgetTokens;
69
- if (request.reasoning?.enabled === false && model.api === "anthropic-messages") {
70
- payloadPatches.push((payload) => ({
71
- ...payload,
72
- thinking: { type: "disabled" },
73
- }));
74
- } else if (requestedReasoningBudget !== undefined && model.api === "anthropic-messages") {
75
- payloadPatches.push((payload) => ({
76
- ...payload,
77
- thinking: {
78
- ...(isRecord(payload.thinking) ? payload.thinking : {}),
79
- type: "enabled",
80
- budget_tokens: requestedReasoningBudget,
81
- },
82
- }));
76
+ options.maxTokens = generation.maxOutputTokens;
77
+ if (generation.topP !== undefined || generation.stop !== undefined) {
78
+ patches.push((payload) => payloadWithSamplingControls(payload, request, api));
83
79
  }
84
- if (model.api === "anthropic-messages") {
85
- // Pi sends `thinking.block_binding: {prefix_mismatch_behavior:
86
- // "drop_block"}` plus the binding-controls beta on mid-conversation-effort
87
- // models (Fable 5.1, Opus 5). That opts every request into the preserved-
88
- // thinking prefix check: when a replayed thinking block's recorded prefix
89
- // no longer matches (client re-render, retry, or provider refusal in the
90
- // loop), the API silently drops the block, which mutates the prompt bytes
91
- // from that block onward and re-writes the rest of the conversation at
92
- // cache-write prices. On organizations not yet enforced (created before
93
- // 2026-08-31), opting out keeps the prefix stable and the cache warm; the
94
- // API records the mismatch without acting on it.
95
- payloadPatches.push((payload) => {
96
- const thinking = payload.thinking;
97
- if (!isRecord(thinking) || !isRecord(thinking.block_binding)) return undefined;
98
- const { block_binding: _dropped, ...thinkingRest } = thinking;
99
- const betas = Array.isArray(payload.betas)
100
- ? (payload.betas as string[]).filter((beta) => beta !== "thinking-binding-controls-2026-08-01")
101
- : payload.betas;
102
- return { ...payload, thinking: thinkingRest, betas };
103
- });
80
+
81
+ if (isAnthropic(api)) {
82
+ const budgetTokens = request.reasoning?.budgetTokens;
83
+ if (request.reasoning?.enabled === false) {
84
+ patches.push((payload) => ({ ...payload, thinking: { type: "disabled" } }));
85
+ } else if (budgetTokens !== undefined) {
86
+ patches.push((payload) => ({
87
+ ...payload,
88
+ thinking: { ...asRecord(payload.thinking), type: "enabled", budget_tokens: budgetTokens },
89
+ }));
90
+ }
91
+ patches.push(payloadWithoutThinkingBlockBinding);
104
92
  }
105
- const gatewayFields = gatewayFor(model.provider)?.requestFields({
106
- zdr: model.zeroDataRetention === true,
107
- });
93
+
94
+ const gatewayFields = gatewayFor(model.provider)?.requestFields({ zdr: model.zeroDataRetention === true });
108
95
  if (gatewayFields !== undefined && Object.keys(gatewayFields).length > 0) {
109
- payloadPatches.push((payload) => payloadWithGatewayFields(payload, gatewayFields));
96
+ patches.push((payload) => payloadWithGatewayFields(payload, gatewayFields));
110
97
  }
111
98
  const safeguards = request.anthropicSafeguards;
112
99
  if (safeguards !== undefined && forwardsAnthropicSafeguards(request, model)) {
113
- payloadPatches.push((payload) => payloadWithSafeguards(payload, safeguards));
100
+ patches.push((payload) => payloadWithSafeguards(payload, safeguards));
114
101
  }
115
- if (
116
- requiredCapabilitiesFor(request).includes("openai_hosted_web_search") &&
117
- isOpenAIResponsesApi(model.api)
118
- ) {
102
+
103
+ if (isOpenAIResponsesApi(api) && requiredCapabilitiesFor(request).includes("openai_hosted_web_search")) {
119
104
  options.emitProviderEvents = true;
120
- if (request.tools.some((tool) => tool.kind === "hosted")) {
121
- payloadPatches.push((payload) => payloadWithHostedTools(payload, request));
105
+ const hosted = request.tools.flatMap((tool) => tool.kind === "hosted" ? [{ ...tool.config }] : []);
106
+ if (hosted.length > 0) {
107
+ patches.push((payload) => ({ ...payload, tools: [...asArray(payload.tools), ...hosted] }));
122
108
  }
123
109
  }
124
- if (
125
- (
126
- model.api === "openai-responses"
127
- || model.api === "azure-openai-responses"
128
- || model.api === "openai-completions"
129
- )
130
- && imageDetailQueues(request).size > 0
131
- ) {
132
- payloadPatches.push((payload) => payloadWithImageDetails(payload, request));
110
+ if (isOpenAI(api)) {
111
+ const details = imageDetailQueues(request);
112
+ if (details.size > 0) patches.push((payload) => payloadWithImageDetails(payload, details));
133
113
  }
134
- const requestedToolChoice = request.toolChoice;
135
- if (requestedToolChoice !== undefined) {
136
- if (isBedrockApi(model.api)) {
137
- // Bedrock builds its Converse toolConfig from options.toolChoice before
138
- // onPayload runs, so the choice must ride on options, not a payload patch.
139
- const simpleChoice = bedrockToolChoice(requestedToolChoice);
140
- if (simpleChoice !== undefined) options.toolChoice = simpleChoice;
141
- } else {
142
- payloadPatches.push((payload) => payloadWithToolChoice(payload, requestedToolChoice, model));
143
- }
114
+
115
+ if (toolChoice !== undefined) {
116
+ // Bedrock builds its Converse toolConfig from options.toolChoice before
117
+ // onPayload runs, so the choice must ride on options, not a payload patch.
118
+ if (isBedrockApi(api)) options.toolChoice = bedrockToolChoice(toolChoice);
119
+ else patches.push((payload) => ({ ...payload, tool_choice: providerToolChoice(toolChoice, api) }));
144
120
  }
145
- if (request.tools.some((tool) =>
146
- tool.kind === "function" && (tool.strict !== undefined || model.api === "anthropic-messages")
147
- )) {
148
- payloadPatches.push((payload) => payloadWithStrictTools(payload, request, model));
121
+ if (request.tools.some((tool) => tool.kind === "function" && (tool.strict !== undefined || isAnthropic(api)))) {
122
+ patches.push((payload) => payloadWithStrictTools(payload, request, api));
149
123
  }
150
- const requestedResponseFormat = request.responseFormat;
151
- if (requestedResponseFormat !== undefined) {
152
- options.responseFormat = responseFormat(requestedResponseFormat);
124
+ if (responseFormat !== undefined) {
125
+ options.responseFormat = piResponseFormat(responseFormat);
153
126
  // Pi keeps responseFormat in options but does not serialize it in simple
154
127
  // OpenAI adapters, so patch the provider payload at the boundary.
155
- payloadPatches.push((payload) => payloadWithResponseFormat(payload, requestedResponseFormat, model));
128
+ patches.push((payload) => payloadWithResponseFormat(payload, responseFormat, api));
156
129
  }
157
- const parallelToolCalls = request.parallelToolCalls;
158
130
  if (parallelToolCalls !== undefined) {
159
- payloadPatches.push((payload) => payloadWithParallelToolCalls(payload, parallelToolCalls, model));
131
+ patches.push((payload) => payloadWithParallelToolCalls(payload, parallelToolCalls, api));
160
132
  }
133
+
161
134
  if (model.fastMode === true) {
162
- if (model.provider === "openai" && model.api === "openai-responses") {
163
- // Pi's simple adapter currently drops serviceTier while lowering its
164
- // options, so patch the provider payload as well. Keep the option for
165
- // usage-pricing fallback once the adapter preserves it end to end.
166
- options.serviceTier = "priority";
167
- payloadPatches.push((payload) => ({ ...payload, service_tier: "priority" }));
168
- } else if (model.provider === "openai" && model.api === "openai-completions") {
169
- payloadPatches.push((payload) => ({ ...payload, service_tier: "priority" }));
170
- } else if (model.provider === "anthropic" && model.api === "anthropic-messages") {
171
- payloadPatches.push((payload) => ({ ...payload, speed: "fast" }));
172
- options.headers = {
173
- ...(isRecord(options.headers) ? options.headers : {}),
174
- "anthropic-beta": ANTHROPIC_FAST_MODE_BETA,
175
- };
135
+ if (model.provider === "openai" && isOpenAI(api) && api !== "azure-openai-responses") {
136
+ // Pi's simple Responses adapter drops serviceTier while lowering its
137
+ // options, so patch the payload too; the option still feeds usage pricing.
138
+ if (api === "openai-responses") options.serviceTier = "priority";
139
+ patches.push((payload) => ({ ...payload, service_tier: "priority" }));
140
+ } else if (model.provider === "anthropic" && isAnthropic(api)) {
141
+ headers["anthropic-beta"] = ANTHROPIC_FAST_MODE_BETA;
142
+ patches.push((payload) => ({ ...payload, speed: "fast" }));
176
143
  }
177
144
  }
178
145
 
179
- const supportedLevels = supportedThinkingLevels(model, pi);
180
- const defaultReasoningEffort = isReasoningEffort(model.defaultThinkingLevel)
181
- ? model.defaultThinkingLevel
182
- : undefined;
183
- const effectiveReasoning = effectiveReasoningForModel({
184
- request,
185
- selectedModel,
186
- supportedLevels,
187
- selectedReasoningEffort,
188
- defaultReasoningEffort,
189
- });
190
- const providerReasoning = request.reasoning?.hasConfigurationUpdate
191
- ? request.reasoning.baseEffort ?? defaultReasoningEffort
192
- : effectiveReasoning;
193
- if (request.reasoning?.baseEffort !== undefined) {
194
- validateReasoningEffortForModel(
195
- request.reasoning.baseEffort,
196
- selectedModel,
197
- supportedLevels,
198
- );
199
- }
200
- if (providerReasoning !== undefined && providerReasoning !== "off") {
201
- options.reasoning = providerReasoning;
202
- } else if (
203
- providerReasoning === "off" &&
204
- offRequiresWireNone(selectedModel) &&
205
- model.api === "openai-completions"
206
- ) {
207
- // These models need explicit "none" rather than the provider default.
208
- // Gateways use nested reasoning; direct providers use reasoning_effort.
209
- payloadPatches.push((payload) => {
210
- if (!gatewayFor(model.provider)) return { ...payload, reasoning_effort: "none" };
211
- const { reasoning_effort: _nativeEffort, ...rest } = payload;
212
- return {
213
- ...rest,
214
- reasoning: { ...(isRecord(payload.reasoning) ? payload.reasoning : {}), effort: "none" },
215
- };
216
- });
217
- }
218
- if (
219
- (requestedToolChoice !== undefined || parallelToolCalls !== undefined) &&
220
- !isBedrockApi(model.api)
221
- ) {
222
- // Some clients send the default tool_choice even after omitting tools for
223
- // a turn. OpenAI rejects every tool_choice value when the lowered payload
224
- // has no tools, so enforce that provider invariant after all tool patches.
225
- payloadPatches.push(payloadWithoutOrphanedToolChoice);
146
+ const reasoning = providerReasoning(request, selectedModel, model, pi, selectedReasoningEffort);
147
+ if (reasoning !== undefined && reasoning !== "off") {
148
+ options.reasoning = reasoning;
149
+ } else if (reasoning === "off" && isCompletions(api) && offRequiresWireNone(selectedModel)) {
150
+ patches.push((payload) => payloadWithReasoningNone(payload, model));
226
151
  }
152
+ if (isOpenAIResponsesApi(api)) patches.push(payloadWithDetailedReasoningSummary);
227
153
 
228
- if (payloadPatches.length > 0) {
229
- options.onPayload = (payload: unknown) => applyPayloadPatches(payload, payloadPatches);
154
+ // Some clients send the default tool_choice even after omitting tools for
155
+ // a turn. OpenAI rejects every tool_choice value when the lowered payload
156
+ // has no tools, so enforce that provider invariant after all tool patches.
157
+ if ((toolChoice !== undefined || parallelToolCalls !== undefined) && !isBedrockApi(api)) {
158
+ patches.push(payloadWithoutOrphanedToolChoice);
230
159
  }
231
- return options;
160
+
161
+ if (Object.keys(headers).length > 0) options.headers = headers;
162
+ if (patches.length > 0) options.onPayload = (payload: unknown) => applyPayloadPatches(payload, patches);
163
+ return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
232
164
  }
233
165
 
234
- function providerAuthenticationOptions(auth: ResolvedProviderAuth): Record<string, unknown> {
235
- if (auth.type === "bearer") return { apiKey: auth.apiKey };
236
- throw new Error("AWS SigV4 authentication requires the Bedrock API.");
166
+ export function supportedThinkingLevels(model: Model, pi: PiOptionsSupport): string[] {
167
+ const levels = Array.isArray(model.supportedThinkingLevels)
168
+ ? model.supportedThinkingLevels
169
+ : pi.getSupportedThinkingLevels?.(model);
170
+ return Array.isArray(levels) ? levels.filter((level) => typeof level === "string") : ["off"];
237
171
  }
238
172
 
239
- function isClaudeCodeOAuth(auth: ResolvedProviderAuth, model: Record<string, unknown>): boolean {
240
- return auth.type === "bearer"
241
- && model.api === "anthropic-messages"
242
- && auth.apiKey.includes("sk-ant-oat");
173
+ function providerReasoning(
174
+ request: InteractionRequest,
175
+ selectedModel: string,
176
+ model: Model,
177
+ pi: PiOptionsSupport,
178
+ selectedReasoningEffort: ReasoningEffort | undefined,
179
+ ): ReasoningEffort | undefined {
180
+ const supportedLevels = supportedThinkingLevels(model, pi);
181
+ const defaultReasoningEffort = isReasoningEffort(model.defaultThinkingLevel) ? model.defaultThinkingLevel : undefined;
182
+ const effective = effectiveReasoningForModel({
183
+ request,
184
+ selectedModel,
185
+ supportedLevels,
186
+ selectedReasoningEffort,
187
+ defaultReasoningEffort,
188
+ });
189
+ const baseEffort = request.reasoning?.baseEffort;
190
+ if (baseEffort !== undefined) validateReasoningEffortForModel(baseEffort, selectedModel, supportedLevels);
191
+ return request.reasoning?.hasConfigurationUpdate ? baseEffort ?? defaultReasoningEffort : effective;
243
192
  }
244
193
 
245
- function bedrockAuthenticationOptions(auth: ResolvedProviderAuth): BedrockOptions {
194
+ function authenticationOptions(auth: ResolvedProviderAuth, api: unknown): BedrockOptions | Record<string, unknown> {
246
195
  if (auth.type === "bearer") return { apiKey: auth.apiKey };
196
+ if (!isBedrockApi(api)) throw new Error("AWS SigV4 authentication requires the Bedrock API.");
247
197
  return {
248
198
  env: {
249
199
  AWS_ACCESS_KEY_ID: auth.access_key_id,
@@ -257,18 +207,13 @@ function bedrockAuthenticationOptions(auth: ResolvedProviderAuth): BedrockOption
257
207
  };
258
208
  }
259
209
 
260
- function bedrockToolChoice(choice: InteractionToolChoice): unknown {
261
- if (typeof choice === "object" && choice.kind === "hosted") return undefined;
262
- if (typeof choice === "string") return choice;
263
- return { type: "function", function: { name: toolWireName(choice.namespace, choice.name) } };
210
+ function isClaudeCodeOAuth(auth: ResolvedProviderAuth, api: unknown): boolean {
211
+ return auth.type === "bearer" && isAnthropic(api) && auth.apiKey.includes("sk-ant-oat");
264
212
  }
265
213
 
266
- // Chains payload patches, preserving each patch's "undefined means leave the
267
- // payload unchanged" contract for the combined result.
268
- function applyPayloadPatches(
269
- payload: unknown,
270
- patches: Array<(payload: Record<string, unknown>) => unknown>
271
- ): unknown {
214
+ // Chains patches, preserving the "undefined means unchanged" contract for the
215
+ // combined result.
216
+ function applyPayloadPatches(payload: unknown, patches: PayloadPatch[]): unknown {
272
217
  if (!isRecord(payload)) return undefined;
273
218
  let current = payload;
274
219
  let changed = false;
@@ -282,305 +227,190 @@ function applyPayloadPatches(
282
227
  return changed ? current : undefined;
283
228
  }
284
229
 
285
- function payloadWithSamplingControls(
286
- payload: Record<string, unknown>,
287
- request: InteractionRequest,
288
- model: Record<string, unknown>,
289
- ): Record<string, unknown> {
290
- const api = model.api;
291
- const google = api === "google-generative-ai" || api === "google-vertex";
292
- const bedrock = isBedrockApi(api);
293
- const stop = request.generation.stop;
294
- const stopSequences = typeof stop === "string" ? [stop] : stop;
295
- const controls = {
296
- // Anthropic rejects top_p with thinking; keep the hosted path aligned
297
- // with the router's unsupported Anthropic top_p control.
298
- ...(request.generation.topP === undefined || api === "anthropic-messages" ? {} : {
299
- [google || bedrock ? "topP" : "top_p"]: request.generation.topP,
300
- }),
301
- ...(stop === undefined || isOpenAIResponsesApi(api) ? {} : {
302
- [google || bedrock ? "stopSequences" : api === "anthropic-messages" ? "stop_sequences" : "stop"]:
303
- api === "openai-completions" ? stop : stopSequences,
304
- }),
305
- };
306
- if (google) {
307
- return { ...payload, config: { ...(isRecord(payload.config) ? payload.config : {}), ...controls } };
308
- }
309
- if (bedrock) {
310
- return {
311
- ...payload,
312
- inferenceConfig: {
313
- ...(isRecord(payload.inferenceConfig) ? payload.inferenceConfig : {}),
314
- ...controls,
315
- },
316
- };
317
- }
230
+ function asRecord(value: unknown): Payload {
231
+ return isRecord(value) ? value : {};
232
+ }
233
+
234
+ function asArray(value: unknown): unknown[] {
235
+ return Array.isArray(value) ? value : [];
236
+ }
237
+
238
+ function payloadWithSamplingControls(payload: Payload, request: InteractionRequest, api: unknown): Payload {
239
+ const { topP, stop } = request.generation;
240
+ const camelCase = isGoogle(api) || isBedrockApi(api);
241
+ const controls: Payload = {};
242
+ // Anthropic rejects top_p with thinking; keep the hosted path aligned with
243
+ // the router's unsupported Anthropic top_p control.
244
+ if (topP !== undefined && !isAnthropic(api)) controls[camelCase ? "topP" : "top_p"] = topP;
245
+ if (stop !== undefined && !isOpenAIResponsesApi(api)) {
246
+ const key = camelCase ? "stopSequences" : isAnthropic(api) ? "stop_sequences" : "stop";
247
+ controls[key] = isCompletions(api) || typeof stop !== "string" ? stop : [stop];
248
+ }
249
+ if (isGoogle(api)) return { ...payload, config: { ...asRecord(payload.config), ...controls } };
250
+ if (isBedrockApi(api)) return { ...payload, inferenceConfig: { ...asRecord(payload.inferenceConfig), ...controls } };
318
251
  return { ...payload, ...controls };
319
252
  }
320
253
 
321
- function responseFormat(format: InteractionResponseFormat): Record<string, unknown> {
322
- if (format.type !== "json_schema") return format;
323
- const { type: _, ...jsonSchema } = format;
324
- return {
325
- type: "json_schema",
326
- jsonSchema,
327
- };
254
+ // Pi sends `thinking.block_binding: {prefix_mismatch_behavior: "drop_block"}`
255
+ // plus the binding-controls beta on mid-conversation-effort models (Fable 5.1,
256
+ // Opus 5). That opts every request into the preserved-thinking prefix check:
257
+ // when a replayed thinking block's recorded prefix no longer matches (client
258
+ // re-render, retry, or provider refusal in the loop), the API silently drops
259
+ // the block, which mutates the prompt bytes from that block onward and
260
+ // re-writes the rest of the conversation at cache-write prices. On
261
+ // organizations not yet enforced (created before 2026-08-31), opting out keeps
262
+ // the prefix stable and the cache warm; the API records the mismatch without
263
+ // acting on it.
264
+ function payloadWithoutThinkingBlockBinding(payload: Payload): Payload | undefined {
265
+ const thinking = payload.thinking;
266
+ if (!isRecord(thinking) || !isRecord(thinking.block_binding)) return undefined;
267
+ const { block_binding: _, ...rest } = thinking;
268
+ const betas = Array.isArray(payload.betas)
269
+ ? payload.betas.filter((beta) => beta !== ANTHROPIC_BINDING_CONTROLS_BETA)
270
+ : payload.betas;
271
+ return { ...payload, thinking: rest, betas };
328
272
  }
329
273
 
330
- function payloadWithResponseFormat(
331
- payload: unknown,
332
- format: InteractionResponseFormat,
333
- model: Record<string, unknown>
334
- ): unknown {
335
- if (!isRecord(payload)) return undefined;
336
- const providerFormat = providerResponseFormat(format, model);
337
- if (providerFormat === null) return undefined;
338
- if (model.api === "openai-responses" || model.api === "azure-openai-responses") {
339
- return {
340
- ...payload,
341
- text: {
342
- ...(isRecord(payload.text) ? payload.text : {}),
343
- format: providerFormat,
344
- },
345
- };
346
- }
347
- if (model.api === "openai-completions") {
348
- return { ...payload, response_format: providerFormat };
349
- }
350
- if (model.api === "anthropic-messages") {
351
- return {
352
- ...payload,
353
- output_config: {
354
- ...(isRecord(payload.output_config) ? payload.output_config : {}),
355
- format: providerFormat,
356
- },
357
- };
358
- }
359
- return undefined;
274
+ // These models need explicit "none" rather than the provider default.
275
+ // Gateways use nested reasoning; direct providers use reasoning_effort.
276
+ function payloadWithReasoningNone(payload: Payload, model: Model): Payload {
277
+ if (!gatewayFor(model.provider)) return { ...payload, reasoning_effort: "none" };
278
+ const { reasoning_effort: _, ...rest } = payload;
279
+ return { ...rest, reasoning: { ...asRecord(payload.reasoning), effort: "none" } };
360
280
  }
361
281
 
362
- function providerResponseFormat(
363
- format: InteractionResponseFormat,
364
- model: Record<string, unknown>
365
- ): Record<string, unknown> | null {
366
- if (format.type === "text") return null;
367
- if (model.api === "anthropic-messages") {
368
- return {
369
- type: "json_schema",
370
- schema: format.type === "json_schema" ? format.schema : { type: "object" },
371
- };
372
- }
373
- if (format.type === "json_object") return format;
374
- const { type: _, ...jsonSchema } = format;
375
- if (model.api === "openai-responses" || model.api === "azure-openai-responses") {
376
- return { ...jsonSchema, type: "json_schema" };
377
- }
378
- return { type: "json_schema", json_schema: jsonSchema };
282
+ // Pi's default "auto" summary leaves many reasoning items empty (93% on the
283
+ // ChatGPT Codex backend). Readable summaries are the only reasoning a later
284
+ // turn on another provider can replay, so ask for detailed ones.
285
+ function payloadWithDetailedReasoningSummary(payload: Payload): Payload | undefined {
286
+ if (!isRecord(payload.reasoning) || payload.reasoning.summary === undefined) return undefined;
287
+ return { ...payload, reasoning: { ...payload.reasoning, summary: "detailed" } };
379
288
  }
380
289
 
381
- function payloadWithToolChoice(
382
- payload: Record<string, unknown>,
383
- choice: InteractionToolChoice,
384
- model: Record<string, unknown>
385
- ): Record<string, unknown> {
386
- if (typeof choice === "object" && choice.kind === "hosted") {
387
- return { ...payload, tool_choice: { type: choice.type } };
388
- }
389
- if (model.api === "anthropic-messages") {
390
- const toolChoice = typeof choice === "string"
391
- ? anthropicToolChoice(choice)
392
- : { type: "tool", name: toolWireName(choice.namespace, choice.name) };
393
- return { ...payload, tool_choice: toolChoice };
394
- }
395
- if (model.api === "openai-responses" || model.api === "azure-openai-responses") {
396
- const toolChoice = typeof choice === "string"
397
- ? openAiToolChoice(choice)
398
- // Pi currently declares both interaction tool kinds as JSON function tools.
399
- : { type: "function", name: toolWireName(choice.namespace, choice.name) };
400
- return { ...payload, tool_choice: toolChoice };
401
- }
402
- const toolChoice = typeof choice === "string"
403
- ? openAiToolChoice(choice)
404
- : { type: "function", function: { name: toolWireName(choice.namespace, choice.name) } };
405
- return { ...payload, tool_choice: toolChoice };
290
+ function piResponseFormat(format: InteractionResponseFormat): Payload {
291
+ if (format.type !== "json_schema") return format;
292
+ const { type: _, ...jsonSchema } = format;
293
+ return { type: "json_schema", jsonSchema };
406
294
  }
407
295
 
408
- function payloadWithoutOrphanedToolChoice(
409
- payload: Record<string, unknown>,
410
- ): Record<string, unknown> | undefined {
411
- if (
412
- !("tool_choice" in payload) ||
413
- (Array.isArray(payload.tools) && payload.tools.length > 0)
414
- ) {
296
+ function payloadWithResponseFormat(payload: Payload, format: InteractionResponseFormat, api: unknown): unknown {
297
+ if (format.type === "text") return undefined;
298
+ if (isAnthropic(api)) {
299
+ const schema = format.type === "json_schema" ? format.schema : { type: "object" };
300
+ return { ...payload, output_config: { ...asRecord(payload.output_config), format: { type: "json_schema", schema } } };
301
+ }
302
+ if (format.type === "json_object") {
303
+ if (isResponses(api)) return { ...payload, text: { ...asRecord(payload.text), format } };
304
+ if (isCompletions(api)) return { ...payload, response_format: format };
415
305
  return undefined;
416
306
  }
417
- const { tool_choice: _, ...withoutToolChoice } = payload;
418
- return withoutToolChoice;
307
+ const { type: _, ...jsonSchema } = format;
308
+ if (isResponses(api)) return { ...payload, text: { ...asRecord(payload.text), format: { ...jsonSchema, type: "json_schema" } } };
309
+ if (isCompletions(api)) return { ...payload, response_format: { type: "json_schema", json_schema: jsonSchema } };
310
+ return undefined;
419
311
  }
420
312
 
421
- function anthropicToolChoice(choice: string): { type: string } {
422
- switch (choice) {
423
- case "required":
424
- case "any":
425
- return { type: "any" };
426
- default:
427
- return { type: choice };
428
- }
313
+ function bedrockToolChoice(choice: InteractionToolChoice): unknown {
314
+ if (typeof choice === "string") return choice;
315
+ if (choice.kind === "hosted") return undefined;
316
+ return { type: "function", function: { name: toolWireName(choice.namespace, choice.name) } };
429
317
  }
430
318
 
431
- function openAiToolChoice(choice: string): string {
432
- return choice === "any" ? "required" : choice;
319
+ function providerToolChoice(choice: InteractionToolChoice, api: unknown): unknown {
320
+ if (typeof choice === "string") {
321
+ if (isAnthropic(api)) return { type: choice === "required" ? "any" : choice };
322
+ return choice === "any" ? "required" : choice;
323
+ }
324
+ if (choice.kind === "hosted") return { type: choice.type };
325
+ const name = toolWireName(choice.namespace, choice.name);
326
+ if (isAnthropic(api)) return { type: "tool", name };
327
+ // Pi currently declares both interaction tool kinds as JSON function tools.
328
+ if (isResponses(api)) return { type: "function", name };
329
+ return { type: "function", function: { name } };
433
330
  }
434
331
 
435
- function payloadWithHostedTools(
436
- payload: Record<string, unknown>,
437
- request: Pick<InteractionRequest, "tools">
438
- ): Record<string, unknown> {
439
- const hosted = request.tools.flatMap((tool) =>
440
- tool.kind === "hosted" ? [{ ...tool.config }] : []
441
- );
442
- if (hosted.length === 0) return payload;
443
- return {
444
- ...payload,
445
- tools: [...(Array.isArray(payload.tools) ? payload.tools : []), ...hosted],
446
- };
332
+ function payloadWithoutOrphanedToolChoice(payload: Payload): Payload | undefined {
333
+ if (!("tool_choice" in payload) || asArray(payload.tools).length > 0) return undefined;
334
+ const { tool_choice: _, ...rest } = payload;
335
+ return rest;
447
336
  }
448
337
 
449
- function payloadWithStrictTools(
450
- payload: Record<string, unknown>,
451
- request: Pick<InteractionRequest, "tools">,
452
- model: Record<string, unknown>
453
- ): unknown {
454
- if (!Array.isArray(payload.tools)) return undefined;
455
- if (
456
- model.api !== "openai-responses" &&
457
- model.api !== "azure-openai-responses" &&
458
- model.api !== "openai-completions" &&
459
- model.api !== "anthropic-messages"
460
- ) {
461
- return undefined;
462
- }
338
+ function payloadWithStrictTools(payload: Payload, request: Pick<InteractionRequest, "tools">, api: unknown): unknown {
339
+ if (!Array.isArray(payload.tools) || !(isOpenAI(api) || isAnthropic(api))) return undefined;
463
340
  const definitions = toolsByWireName(request.tools);
464
- return {
465
- ...payload,
466
- tools: payload.tools.map((entry) => {
467
- if (!isRecord(entry)) return entry;
468
- if (model.api === "anthropic-messages") {
469
- const tool = typeof entry.name === "string" ? definitions.get(entry.name) : undefined;
470
- if (!tool || tool.kind !== "function") return entry;
471
- // defer_loading is intentionally not restored from the definition:
472
- // Pi decides placement (deferred batch vs force-loaded immediate), and
473
- // re-adding it here would recreate the cache_control/defer_loading
474
- // combination Anthropic rejects. The entry spread preserves Pi's choice.
475
- return {
476
- ...entry,
477
- ...(tool.parameters !== undefined ? { input_schema: tool.parameters } : {}),
478
- ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
479
- };
480
- }
481
- const definition = model.api === "openai-completions" && isRecord(entry.function) ? entry.function : entry;
482
- const tool = typeof definition.name === "string" ? definitions.get(definition.name) : undefined;
483
- if (!tool || tool.kind !== "function" || tool.strict === undefined) return entry;
484
- if (model.api === "openai-completions") {
485
- return { ...entry, function: { ...definition, strict: tool.strict } };
486
- }
487
- return { ...entry, strict: tool.strict };
488
- }),
341
+ const functionTool = (name: unknown) => {
342
+ const tool = typeof name === "string" ? definitions.get(name) : undefined;
343
+ return tool?.kind === "function" ? tool : undefined;
489
344
  };
345
+ const tools = payload.tools.map((entry) => {
346
+ if (!isRecord(entry)) return entry;
347
+ if (isAnthropic(api)) {
348
+ const tool = functionTool(entry.name);
349
+ if (!tool) return entry;
350
+ // defer_loading is intentionally not restored from the definition: Pi
351
+ // decides placement (deferred batch vs force-loaded immediate), and
352
+ // re-adding it here would recreate the cache_control/defer_loading
353
+ // combination Anthropic rejects. The entry spread preserves Pi's choice.
354
+ return {
355
+ ...entry,
356
+ ...(tool.parameters !== undefined ? { input_schema: tool.parameters } : {}),
357
+ ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
358
+ };
359
+ }
360
+ if (isCompletions(api) && isRecord(entry.function)) {
361
+ const strict = functionTool(entry.function.name)?.strict;
362
+ return strict === undefined ? entry : { ...entry, function: { ...entry.function, strict } };
363
+ }
364
+ const strict = functionTool(entry.name)?.strict;
365
+ return strict === undefined ? entry : { ...entry, strict };
366
+ });
367
+ return { ...payload, tools };
490
368
  }
491
369
 
492
- function payloadWithParallelToolCalls(
493
- payload: Record<string, unknown>,
494
- enabled: boolean,
495
- model: Record<string, unknown>
496
- ): unknown {
497
- if (model.api === "openai-responses" || model.api === "azure-openai-responses" || model.api === "openai-completions") {
498
- return { ...payload, parallel_tool_calls: enabled };
499
- }
500
- if (model.api === "anthropic-messages" && !enabled) {
501
- const current = isRecord(payload.tool_choice) ? payload.tool_choice : {};
502
- if (current.type === "none") return undefined;
503
- return {
504
- ...payload,
505
- tool_choice: {
506
- type: typeof current.type === "string" ? current.type : "auto",
507
- ...current,
508
- disable_parallel_tool_use: true,
509
- },
510
- };
511
- }
512
- return undefined;
370
+ function payloadWithParallelToolCalls(payload: Payload, enabled: boolean, api: unknown): unknown {
371
+ if (isOpenAI(api)) return { ...payload, parallel_tool_calls: enabled };
372
+ if (!isAnthropic(api) || enabled) return undefined;
373
+ const current = asRecord(payload.tool_choice);
374
+ if (current.type === "none") return undefined;
375
+ return { ...payload, tool_choice: { type: "auto", ...current, disable_parallel_tool_use: true } };
513
376
  }
514
377
 
515
- function payloadWithImageDetails(
516
- payload: Record<string, unknown>,
517
- request: InteractionRequest
518
- ): unknown {
519
- const details = imageDetailQueues(request);
520
- if (details.size === 0) return undefined;
378
+ // Fills `detail` on Responses (`input_image`) and Chat (`image_url`) parts,
379
+ // matching request images to payload images by URL in order.
380
+ function payloadWithImageDetails(payload: Payload, details: Map<string, ImageDetail[]>): unknown {
381
+ const queues = new Map([...details].map(([url, queue]) => [url, [...queue]]));
521
382
  let changed = false;
522
-
523
383
  const patch = (value: unknown): unknown => {
524
384
  if (Array.isArray(value)) return value.map(patch);
525
385
  if (!isRecord(value)) return value;
526
-
527
- const mapped = Object.fromEntries(
528
- Object.entries(value).map(([key, entry]) => [key, patch(entry)])
529
- );
530
- const responseUrl = mapped.type === "input_image" && typeof mapped.image_url === "string"
531
- ? mapped.image_url
532
- : undefined;
533
- const chatImage = mapped.type === "image_url" && isRecord(mapped.image_url)
534
- ? mapped.image_url
535
- : undefined;
536
- const chatUrl = typeof chatImage?.url === "string" ? chatImage.url : undefined;
537
- const url = responseUrl ?? chatUrl;
538
- if (!url) return mapped;
539
-
540
- const queue = details.get(url);
541
- const detail = queue?.shift();
542
- if (detail === undefined) return mapped;
543
- changed = true;
544
- if (responseUrl) return { ...mapped, detail };
545
- return { ...mapped, image_url: { ...chatImage, detail } };
386
+ const node = Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, patch(entry)]));
387
+ if (node.type === "input_image" && typeof node.image_url === "string") {
388
+ const detail = queues.get(node.image_url)?.shift();
389
+ if (detail === undefined) return node;
390
+ changed = true;
391
+ return { ...node, detail };
392
+ }
393
+ if (node.type === "image_url" && isRecord(node.image_url) && typeof node.image_url.url === "string") {
394
+ const detail = queues.get(node.image_url.url)?.shift();
395
+ if (detail === undefined) return node;
396
+ changed = true;
397
+ return { ...node, image_url: { ...node.image_url, detail } };
398
+ }
399
+ return node;
546
400
  };
547
-
548
401
  const patched = patch(payload);
549
- return changed && isRecord(patched) ? patched : undefined;
402
+ return changed ? patched : undefined;
550
403
  }
551
404
 
552
- function imageDetailQueues(
553
- request: InteractionRequest
554
- ): Map<string, Array<"auto" | "low" | "high" | "original">> {
555
- const details = new Map<string, Array<"auto" | "low" | "high" | "original">>();
556
- const add = (content: unknown) => {
557
- if (!Array.isArray(content)) return;
558
- for (const part of content) {
405
+ function imageDetailQueues(request: InteractionRequest): Map<string, ImageDetail[]> {
406
+ const details = new Map<string, ImageDetail[]>();
407
+ for (const item of request.items) {
408
+ const content = item.type === "message" ? item.content : item.type === "tool_result" ? item.output : undefined;
409
+ for (const part of asArray(content)) {
559
410
  if (!isRecord(part) || part.type !== "image" || typeof part.url !== "string") continue;
560
- if (part.detail !== "auto" && part.detail !== "low" && part.detail !== "high" && part.detail !== "original") continue;
561
- const queue = details.get(part.url) ?? [];
562
- queue.push(part.detail);
563
- details.set(part.url, queue);
411
+ if (!IMAGE_DETAILS.includes(part.detail)) continue;
412
+ details.set(part.url, [...(details.get(part.url) ?? []), part.detail as ImageDetail]);
564
413
  }
565
- };
566
- for (const item of request.items) {
567
- if (item.type === "message") add(item.content);
568
- if (item.type === "tool_result") add(item.output);
569
414
  }
570
415
  return details;
571
416
  }
572
-
573
- export function supportedThinkingLevels(
574
- model: Record<string, unknown>,
575
- pi: PiOptionsSupport
576
- ): string[] {
577
- const explicitLevels = model.supportedThinkingLevels;
578
- if (Array.isArray(explicitLevels)) {
579
- return explicitLevels.filter((level) => typeof level === "string");
580
- }
581
- const levels = pi.getSupportedThinkingLevels?.(model);
582
- if (Array.isArray(levels)) {
583
- return levels.filter((level) => typeof level === "string");
584
- }
585
- return ["off"];
586
- }