@linxiraos/pi-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (639) hide show
  1. package/CHANGELOG.md +5066 -0
  2. package/README.md +1195 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  5. package/dist/types/auth-broker/client.d.ts +93 -0
  6. package/dist/types/auth-broker/discover.d.ts +39 -0
  7. package/dist/types/auth-broker/index.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +136 -0
  10. package/dist/types/auth-broker/server.d.ts +48 -0
  11. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  12. package/dist/types/auth-broker/types.d.ts +152 -0
  13. package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
  14. package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
  15. package/dist/types/auth-gateway/http.d.ts +56 -0
  16. package/dist/types/auth-gateway/index.d.ts +3 -0
  17. package/dist/types/auth-gateway/server.d.ts +36 -0
  18. package/dist/types/auth-gateway/types.d.ts +127 -0
  19. package/dist/types/auth-retry.d.ts +150 -0
  20. package/dist/types/auth-storage.d.ts +1258 -0
  21. package/dist/types/dialect/anthropic.d.ts +15 -0
  22. package/dist/types/dialect/catalog.d.ts +3 -0
  23. package/dist/types/dialect/coercion.d.ts +23 -0
  24. package/dist/types/dialect/deepseek.d.ts +14 -0
  25. package/dist/types/dialect/demotion.d.ts +30 -0
  26. package/dist/types/dialect/examples.d.ts +18 -0
  27. package/dist/types/dialect/factory.d.ts +3 -0
  28. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  29. package/dist/types/dialect/gemini.d.ts +17 -0
  30. package/dist/types/dialect/gemma.d.ts +15 -0
  31. package/dist/types/dialect/glm.d.ts +9 -0
  32. package/dist/types/dialect/harmony.d.ts +8 -0
  33. package/dist/types/dialect/hermes.d.ts +9 -0
  34. package/dist/types/dialect/history.d.ts +3 -0
  35. package/dist/types/dialect/index.d.ts +11 -0
  36. package/dist/types/dialect/inventory.d.ts +9 -0
  37. package/dist/types/dialect/kimi.d.ts +14 -0
  38. package/dist/types/dialect/minimax.d.ts +3 -0
  39. package/dist/types/dialect/owned-stream.d.ts +4 -0
  40. package/dist/types/dialect/qwen3.d.ts +9 -0
  41. package/dist/types/dialect/rendering.d.ts +54 -0
  42. package/dist/types/dialect/thinking.d.ts +6 -0
  43. package/dist/types/dialect/types.d.ts +68 -0
  44. package/dist/types/dialect/xml.d.ts +9 -0
  45. package/dist/types/error/abort.d.ts +14 -0
  46. package/dist/types/error/auth-classify.d.ts +20 -0
  47. package/dist/types/error/auth.d.ts +27 -0
  48. package/dist/types/error/aws.d.ts +27 -0
  49. package/dist/types/error/classes.d.ts +106 -0
  50. package/dist/types/error/finalize.d.ts +39 -0
  51. package/dist/types/error/flags.d.ts +94 -0
  52. package/dist/types/error/format.d.ts +20 -0
  53. package/dist/types/error/gateway.d.ts +20 -0
  54. package/dist/types/error/index.d.ts +14 -0
  55. package/dist/types/error/oauth.d.ts +43 -0
  56. package/dist/types/error/provider.d.ts +42 -0
  57. package/dist/types/error/rate-limit.d.ts +80 -0
  58. package/dist/types/error/retryable.d.ts +27 -0
  59. package/dist/types/error/validation.d.ts +32 -0
  60. package/dist/types/index.d.ts +51 -0
  61. package/dist/types/provider-details.d.ts +24 -0
  62. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  63. package/dist/types/providers/anthropic-client.d.ts +106 -0
  64. package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
  65. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  66. package/dist/types/providers/anthropic-wire.d.ts +345 -0
  67. package/dist/types/providers/anthropic.d.ts +261 -0
  68. package/dist/types/providers/aws-credentials.d.ts +48 -0
  69. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  70. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  71. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  72. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  73. package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
  74. package/dist/types/providers/cowork-fetch.d.ts +3 -0
  75. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  76. package/dist/types/providers/cursor-pi-args.d.ts +105 -0
  77. package/dist/types/providers/cursor.d.ts +221 -0
  78. package/dist/types/providers/devin.d.ts +12 -0
  79. package/dist/types/providers/error-message.d.ts +25 -0
  80. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  81. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  82. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  83. package/dist/types/providers/google-auth.d.ts +24 -0
  84. package/dist/types/providers/google-gemini-cli.d.ts +120 -0
  85. package/dist/types/providers/google-shared.d.ts +203 -0
  86. package/dist/types/providers/google-types.d.ts +155 -0
  87. package/dist/types/providers/google-vertex.d.ts +7 -0
  88. package/dist/types/providers/google.d.ts +4 -0
  89. package/dist/types/providers/grammar.d.ts +1 -0
  90. package/dist/types/providers/kimi.d.ts +27 -0
  91. package/dist/types/providers/mock.d.ts +179 -0
  92. package/dist/types/providers/ollama.d.ts +8 -0
  93. package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
  94. package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
  95. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  96. package/dist/types/providers/openai-chat-wire.d.ts +669 -0
  97. package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
  98. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  99. package/dist/types/providers/openai-codex-responses.d.ts +238 -0
  100. package/dist/types/providers/openai-completions.d.ts +48 -0
  101. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  102. package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
  103. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  104. package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
  105. package/dist/types/providers/openai-responses.d.ts +131 -0
  106. package/dist/types/providers/openai-shared.d.ts +624 -0
  107. package/dist/types/providers/pi-native-client.d.ts +13 -0
  108. package/dist/types/providers/pi-native-server.d.ts +69 -0
  109. package/dist/types/providers/register-builtins.d.ts +37 -0
  110. package/dist/types/providers/synthetic.d.ts +26 -0
  111. package/dist/types/providers/transform-messages.d.ts +32 -0
  112. package/dist/types/providers/vision-guard.d.ts +20 -0
  113. package/dist/types/registry/aiand.d.ts +7 -0
  114. package/dist/types/registry/aimlapi.d.ts +4 -0
  115. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  116. package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
  117. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  118. package/dist/types/registry/anthropic.d.ts +10 -0
  119. package/dist/types/registry/api-key-login.d.ts +42 -0
  120. package/dist/types/registry/api-key-validation.d.ts +43 -0
  121. package/dist/types/registry/aws.d.ts +13 -0
  122. package/dist/types/registry/azure.d.ts +4 -0
  123. package/dist/types/registry/baseten.d.ts +7 -0
  124. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  125. package/dist/types/registry/cerebras.d.ts +7 -0
  126. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  127. package/dist/types/registry/coreweave.d.ts +7 -0
  128. package/dist/types/registry/cursor.d.ts +7 -0
  129. package/dist/types/registry/deepseek.d.ts +8 -0
  130. package/dist/types/registry/derived.d.ts +5 -0
  131. package/dist/types/registry/devin.d.ts +8 -0
  132. package/dist/types/registry/exa.d.ts +8 -0
  133. package/dist/types/registry/firepass.d.ts +16 -0
  134. package/dist/types/registry/fireworks.d.ts +7 -0
  135. package/dist/types/registry/github-copilot.d.ts +7 -0
  136. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  137. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  138. package/dist/types/registry/gmi-cloud.d.ts +7 -0
  139. package/dist/types/registry/google-antigravity.d.ts +9 -0
  140. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  141. package/dist/types/registry/google-vertex.d.ts +5 -0
  142. package/dist/types/registry/google.d.ts +4 -0
  143. package/dist/types/registry/groq.d.ts +4 -0
  144. package/dist/types/registry/huggingface.d.ts +7 -0
  145. package/dist/types/registry/index.d.ts +4 -0
  146. package/dist/types/registry/kagi.d.ts +14 -0
  147. package/dist/types/registry/kilo.d.ts +7 -0
  148. package/dist/types/registry/kimi-code.d.ts +7 -0
  149. package/dist/types/registry/litellm.d.ts +13 -0
  150. package/dist/types/registry/llama-cpp.d.ts +8 -0
  151. package/dist/types/registry/lm-studio.d.ts +8 -0
  152. package/dist/types/registry/meta.d.ts +7 -0
  153. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  154. package/dist/types/registry/minimax-code.d.ts +6 -0
  155. package/dist/types/registry/minimax.d.ts +4 -0
  156. package/dist/types/registry/mistral.d.ts +4 -0
  157. package/dist/types/registry/moonshot.d.ts +7 -0
  158. package/dist/types/registry/nanogpt.d.ts +7 -0
  159. package/dist/types/registry/novita.d.ts +6 -0
  160. package/dist/types/registry/nvidia.d.ts +7 -0
  161. package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
  162. package/dist/types/registry/oauth/anthropic.d.ts +24 -0
  163. package/dist/types/registry/oauth/callback-server.d.ts +74 -0
  164. package/dist/types/registry/oauth/cursor.d.ts +16 -0
  165. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  166. package/dist/types/registry/oauth/devin.d.ts +5 -0
  167. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  168. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  169. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  170. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  171. package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
  172. package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
  173. package/dist/types/registry/oauth/index.d.ts +45 -0
  174. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  175. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  176. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  177. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  178. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  179. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  180. package/dist/types/registry/oauth/types.d.ts +87 -0
  181. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  182. package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
  183. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  184. package/dist/types/registry/oauth/zai.d.ts +25 -0
  185. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  186. package/dist/types/registry/ollama.d.ts +12 -0
  187. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  188. package/dist/types/registry/openai-codex.d.ts +9 -0
  189. package/dist/types/registry/openai.d.ts +4 -0
  190. package/dist/types/registry/opencode-go.d.ts +6 -0
  191. package/dist/types/registry/opencode-zen.d.ts +6 -0
  192. package/dist/types/registry/openrouter.d.ts +13 -0
  193. package/dist/types/registry/parallel.d.ts +14 -0
  194. package/dist/types/registry/perplexity.d.ts +7 -0
  195. package/dist/types/registry/qianfan.d.ts +7 -0
  196. package/dist/types/registry/qwen-portal.d.ts +7 -0
  197. package/dist/types/registry/registry.d.ts +367 -0
  198. package/dist/types/registry/sakana.d.ts +7 -0
  199. package/dist/types/registry/siliconflow-cn.d.ts +7 -0
  200. package/dist/types/registry/siliconflow.d.ts +7 -0
  201. package/dist/types/registry/synthetic.d.ts +6 -0
  202. package/dist/types/registry/tavily.d.ts +14 -0
  203. package/dist/types/registry/together.d.ts +6 -0
  204. package/dist/types/registry/types.d.ts +75 -0
  205. package/dist/types/registry/umans.d.ts +7 -0
  206. package/dist/types/registry/venice.d.ts +13 -0
  207. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  208. package/dist/types/registry/vllm.d.ts +7 -0
  209. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  210. package/dist/types/registry/xai-oauth.d.ts +7 -0
  211. package/dist/types/registry/xai.d.ts +7 -0
  212. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  213. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  214. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  215. package/dist/types/registry/xiaomi.d.ts +6 -0
  216. package/dist/types/registry/zai.d.ts +15 -0
  217. package/dist/types/registry/zenmux.d.ts +7 -0
  218. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  219. package/dist/types/stream.d.ts +46 -0
  220. package/dist/types/types.d.ts +1064 -0
  221. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  222. package/dist/types/usage/claude.d.ts +4 -0
  223. package/dist/types/usage/cursor.d.ts +4 -0
  224. package/dist/types/usage/gemini.d.ts +2 -0
  225. package/dist/types/usage/github-copilot.d.ts +7 -0
  226. package/dist/types/usage/google-antigravity.d.ts +15 -0
  227. package/dist/types/usage/kimi.d.ts +2 -0
  228. package/dist/types/usage/minimax-code.d.ts +3 -0
  229. package/dist/types/usage/ollama.d.ts +5 -0
  230. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  231. package/dist/types/usage/openai-codex-reset.d.ts +88 -0
  232. package/dist/types/usage/openai-codex.d.ts +10 -0
  233. package/dist/types/usage/opencode-go.d.ts +2 -0
  234. package/dist/types/usage/shared.d.ts +1 -0
  235. package/dist/types/usage/synthetic.d.ts +2 -0
  236. package/dist/types/usage/umans.d.ts +2 -0
  237. package/dist/types/usage/xai-oauth.d.ts +12 -0
  238. package/dist/types/usage/zai.d.ts +3 -0
  239. package/dist/types/usage.d.ts +527 -0
  240. package/dist/types/utils/abort.d.ts +25 -0
  241. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  242. package/dist/types/utils/aws-profile.d.ts +17 -0
  243. package/dist/types/utils/block-symbols.d.ts +62 -0
  244. package/dist/types/utils/deterministic-id.d.ts +16 -0
  245. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  246. package/dist/types/utils/event-stream.d.ts +39 -0
  247. package/dist/types/utils/foundry.d.ts +1 -0
  248. package/dist/types/utils/google-validation.d.ts +2 -0
  249. package/dist/types/utils/harmony-leak.d.ts +135 -0
  250. package/dist/types/utils/http-inspector.d.ts +49 -0
  251. package/dist/types/utils/idle-iterator.d.ts +149 -0
  252. package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
  253. package/dist/types/utils/openai-http.d.ts +48 -0
  254. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  255. package/dist/types/utils/parse-bind.d.ts +23 -0
  256. package/dist/types/utils/provider-response.d.ts +3 -0
  257. package/dist/types/utils/proxy.d.ts +39 -0
  258. package/dist/types/utils/request-debug.d.ts +29 -0
  259. package/dist/types/utils/retry-after.d.ts +4 -0
  260. package/dist/types/utils/retry.d.ts +14 -0
  261. package/dist/types/utils/schema/adapt.d.ts +24 -0
  262. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  263. package/dist/types/utils/schema/dereference.d.ts +11 -0
  264. package/dist/types/utils/schema/draft.d.ts +10 -0
  265. package/dist/types/utils/schema/equality.d.ts +4 -0
  266. package/dist/types/utils/schema/fields.d.ts +54 -0
  267. package/dist/types/utils/schema/index.d.ts +14 -0
  268. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  269. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  270. package/dist/types/utils/schema/normalize.d.ts +153 -0
  271. package/dist/types/utils/schema/spill.d.ts +8 -0
  272. package/dist/types/utils/schema/stamps.d.ts +17 -0
  273. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  274. package/dist/types/utils/schema/types.d.ts +4 -0
  275. package/dist/types/utils/schema/typescript.d.ts +24 -0
  276. package/dist/types/utils/schema/wire.d.ts +52 -0
  277. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  278. package/dist/types/utils/sse-debug.d.ts +5 -0
  279. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  280. package/dist/types/utils/thinking-loop.d.ts +102 -0
  281. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  282. package/dist/types/utils/tool-choice.d.ts +52 -0
  283. package/dist/types/utils/validation.d.ts +28 -0
  284. package/dist/types/utils.d.ts +57 -0
  285. package/package.json +138 -0
  286. package/src/api-registry.ts +109 -0
  287. package/src/auth/sqlite-credential-store.ts +2066 -0
  288. package/src/auth-broker/client.ts +471 -0
  289. package/src/auth-broker/discover.ts +310 -0
  290. package/src/auth-broker/index.ts +7 -0
  291. package/src/auth-broker/refresher.ts +117 -0
  292. package/src/auth-broker/remote-store.ts +1332 -0
  293. package/src/auth-broker/server.ts +898 -0
  294. package/src/auth-broker/snapshot-cache.ts +200 -0
  295. package/src/auth-broker/types.ts +193 -0
  296. package/src/auth-broker/wire-schema-resource.ts +487 -0
  297. package/src/auth-broker/wire-schemas.ts +43 -0
  298. package/src/auth-gateway/http.ts +227 -0
  299. package/src/auth-gateway/index.ts +3 -0
  300. package/src/auth-gateway/server.ts +836 -0
  301. package/src/auth-gateway/types.ts +153 -0
  302. package/src/auth-retry.ts +401 -0
  303. package/src/auth-storage.ts +6540 -0
  304. package/src/dialect/anthropic.md +31 -0
  305. package/src/dialect/anthropic.ts +608 -0
  306. package/src/dialect/catalog.ts +29 -0
  307. package/src/dialect/coercion.ts +136 -0
  308. package/src/dialect/deepseek.md +24 -0
  309. package/src/dialect/deepseek.ts +609 -0
  310. package/src/dialect/demotion.ts +40 -0
  311. package/src/dialect/examples.ts +71 -0
  312. package/src/dialect/factory.ts +34 -0
  313. package/src/dialect/fenced-thinking.ts +184 -0
  314. package/src/dialect/gemini.md +44 -0
  315. package/src/dialect/gemini.ts +583 -0
  316. package/src/dialect/gemma.md +33 -0
  317. package/src/dialect/gemma.ts +387 -0
  318. package/src/dialect/glm.md +32 -0
  319. package/src/dialect/glm.ts +579 -0
  320. package/src/dialect/harmony.md +31 -0
  321. package/src/dialect/harmony.ts +345 -0
  322. package/src/dialect/hermes.md +25 -0
  323. package/src/dialect/hermes.ts +206 -0
  324. package/src/dialect/history.ts +81 -0
  325. package/src/dialect/index.ts +15 -0
  326. package/src/dialect/inventory.ts +30 -0
  327. package/src/dialect/kimi.md +24 -0
  328. package/src/dialect/kimi.ts +340 -0
  329. package/src/dialect/minimax.md +31 -0
  330. package/src/dialect/minimax.ts +95 -0
  331. package/src/dialect/owned-stream.ts +481 -0
  332. package/src/dialect/prompt-template.md +12 -0
  333. package/src/dialect/qwen3.md +28 -0
  334. package/src/dialect/qwen3.ts +240 -0
  335. package/src/dialect/rendering.ts +304 -0
  336. package/src/dialect/thinking.ts +292 -0
  337. package/src/dialect/types.ts +56 -0
  338. package/src/dialect/xml.md +22 -0
  339. package/src/dialect/xml.ts +90 -0
  340. package/src/error/abort.ts +18 -0
  341. package/src/error/auth-classify.ts +47 -0
  342. package/src/error/auth.ts +48 -0
  343. package/src/error/aws.ts +35 -0
  344. package/src/error/classes.ts +281 -0
  345. package/src/error/finalize.ts +69 -0
  346. package/src/error/flags.ts +602 -0
  347. package/src/error/format.ts +45 -0
  348. package/src/error/gateway.ts +96 -0
  349. package/src/error/index.ts +14 -0
  350. package/src/error/oauth.ts +58 -0
  351. package/src/error/provider.ts +63 -0
  352. package/src/error/rate-limit.ts +303 -0
  353. package/src/error/retryable.ts +70 -0
  354. package/src/error/validation.ts +44 -0
  355. package/src/index.ts +51 -0
  356. package/src/provider-details.ts +90 -0
  357. package/src/providers/amazon-bedrock.ts +1064 -0
  358. package/src/providers/anthropic-client.ts +317 -0
  359. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  360. package/src/providers/anthropic-messages-server.ts +818 -0
  361. package/src/providers/anthropic-wire.ts +359 -0
  362. package/src/providers/anthropic.ts +4539 -0
  363. package/src/providers/aws-credentials.ts +772 -0
  364. package/src/providers/aws-eventstream.ts +181 -0
  365. package/src/providers/aws-sigv4.ts +218 -0
  366. package/src/providers/azure-openai-responses.ts +438 -0
  367. package/src/providers/bedrock-mantle.ts +110 -0
  368. package/src/providers/claude-code-fingerprint.ts +20 -0
  369. package/src/providers/cowork-fetch.ts +201 -0
  370. package/src/providers/cursor/exec-modern.ts +496 -0
  371. package/src/providers/cursor/proto/agent.proto +4533 -0
  372. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  373. package/src/providers/cursor/proto/buf.yaml +17 -0
  374. package/src/providers/cursor-pi-args.ts +165 -0
  375. package/src/providers/cursor.ts +4689 -0
  376. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  377. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  378. package/src/providers/devin/proto/buf.yaml +17 -0
  379. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  380. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  381. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  382. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  383. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  384. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  385. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  386. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  387. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  388. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  389. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  390. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  391. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  392. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  393. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  394. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  395. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  396. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  397. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  398. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  399. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  400. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  401. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  402. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  403. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  404. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  405. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  406. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  407. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  408. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  409. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  410. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  411. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  412. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  413. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  414. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  415. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  416. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  417. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  418. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  419. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  420. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  421. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  422. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  423. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  424. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  425. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  426. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  427. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  428. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  429. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  430. package/src/providers/devin.ts +679 -0
  431. package/src/providers/error-message.ts +23 -0
  432. package/src/providers/github-copilot-headers.ts +141 -0
  433. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  434. package/src/providers/gitlab-duo-workflow.ts +3135 -0
  435. package/src/providers/gitlab-duo.ts +399 -0
  436. package/src/providers/google-auth.ts +330 -0
  437. package/src/providers/google-gemini-cli.ts +1370 -0
  438. package/src/providers/google-shared.ts +1122 -0
  439. package/src/providers/google-types.ts +180 -0
  440. package/src/providers/google-vertex.ts +135 -0
  441. package/src/providers/google.ts +47 -0
  442. package/src/providers/grammar.ts +70 -0
  443. package/src/providers/kimi.ts +51 -0
  444. package/src/providers/mock.ts +514 -0
  445. package/src/providers/ollama.ts +776 -0
  446. package/src/providers/openai-anthropic-shim.ts +166 -0
  447. package/src/providers/openai-chat-server-schema.ts +243 -0
  448. package/src/providers/openai-chat-server.ts +752 -0
  449. package/src/providers/openai-chat-wire.ts +859 -0
  450. package/src/providers/openai-codex/request-transformer.ts +491 -0
  451. package/src/providers/openai-codex/response-handler.ts +102 -0
  452. package/src/providers/openai-codex-responses.ts +4716 -0
  453. package/src/providers/openai-completions.ts +2389 -0
  454. package/src/providers/openai-reasoning-fallback.ts +269 -0
  455. package/src/providers/openai-responses-server-schema.ts +397 -0
  456. package/src/providers/openai-responses-server.ts +1466 -0
  457. package/src/providers/openai-responses-wire.ts +6416 -0
  458. package/src/providers/openai-responses.ts +1393 -0
  459. package/src/providers/openai-shared.ts +3500 -0
  460. package/src/providers/pi-native-client.ts +275 -0
  461. package/src/providers/pi-native-server.ts +245 -0
  462. package/src/providers/register-builtins.ts +503 -0
  463. package/src/providers/synthetic.ts +50 -0
  464. package/src/providers/transform-messages.ts +1083 -0
  465. package/src/providers/vision-guard.ts +54 -0
  466. package/src/registry/aiand.ts +22 -0
  467. package/src/registry/aimlapi.ts +6 -0
  468. package/src/registry/alibaba-coding-plan.ts +104 -0
  469. package/src/registry/alibaba-token-plan.ts +125 -0
  470. package/src/registry/amazon-bedrock.ts +22 -0
  471. package/src/registry/anthropic.ts +26 -0
  472. package/src/registry/api-key-login.ts +115 -0
  473. package/src/registry/api-key-validation.ts +145 -0
  474. package/src/registry/aws.ts +57 -0
  475. package/src/registry/azure.ts +6 -0
  476. package/src/registry/baseten.ts +22 -0
  477. package/src/registry/bedrock-mantle.ts +34 -0
  478. package/src/registry/cerebras.ts +23 -0
  479. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  480. package/src/registry/coreweave.ts +40 -0
  481. package/src/registry/cursor.ts +20 -0
  482. package/src/registry/deepseek.ts +46 -0
  483. package/src/registry/derived.ts +9 -0
  484. package/src/registry/devin.ts +15 -0
  485. package/src/registry/exa.ts +19 -0
  486. package/src/registry/firepass.ts +32 -0
  487. package/src/registry/fireworks.ts +28 -0
  488. package/src/registry/github-copilot.ts +22 -0
  489. package/src/registry/gitlab-duo-workflow.ts +20 -0
  490. package/src/registry/gitlab-duo.ts +19 -0
  491. package/src/registry/gmi-cloud.ts +22 -0
  492. package/src/registry/google-antigravity.ts +22 -0
  493. package/src/registry/google-gemini-cli.ts +22 -0
  494. package/src/registry/google-vertex.ts +38 -0
  495. package/src/registry/google.ts +6 -0
  496. package/src/registry/groq.ts +6 -0
  497. package/src/registry/huggingface.ts +29 -0
  498. package/src/registry/index.ts +4 -0
  499. package/src/registry/kagi.ts +46 -0
  500. package/src/registry/kilo.ts +114 -0
  501. package/src/registry/kimi-code.ts +17 -0
  502. package/src/registry/litellm.ts +45 -0
  503. package/src/registry/llama-cpp.ts +35 -0
  504. package/src/registry/lm-studio.ts +31 -0
  505. package/src/registry/meta.ts +22 -0
  506. package/src/registry/minimax-code-cn.ts +12 -0
  507. package/src/registry/minimax-code.ts +12 -0
  508. package/src/registry/minimax.ts +6 -0
  509. package/src/registry/mistral.ts +6 -0
  510. package/src/registry/moonshot.ts +28 -0
  511. package/src/registry/nanogpt.ts +22 -0
  512. package/src/registry/novita.ts +25 -0
  513. package/src/registry/nvidia.ts +61 -0
  514. package/src/registry/oauth/anthropic-constants.ts +12 -0
  515. package/src/registry/oauth/anthropic.ts +346 -0
  516. package/src/registry/oauth/callback-server.ts +438 -0
  517. package/src/registry/oauth/cursor.ts +187 -0
  518. package/src/registry/oauth/device-code.ts +92 -0
  519. package/src/registry/oauth/devin.ts +124 -0
  520. package/src/registry/oauth/github-copilot.ts +369 -0
  521. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  522. package/src/registry/oauth/gitlab-duo.ts +222 -0
  523. package/src/registry/oauth/google-antigravity.ts +225 -0
  524. package/src/registry/oauth/google-gemini-cli.ts +297 -0
  525. package/src/registry/oauth/google-oauth-shared.ts +211 -0
  526. package/src/registry/oauth/index.ts +187 -0
  527. package/src/registry/oauth/kimi.ts +297 -0
  528. package/src/registry/oauth/minimax-code.ts +53 -0
  529. package/src/registry/oauth/oauth.html +317 -0
  530. package/src/registry/oauth/openai-codex.ts +384 -0
  531. package/src/registry/oauth/opencode.ts +50 -0
  532. package/src/registry/oauth/perplexity.ts +228 -0
  533. package/src/registry/oauth/pkce.ts +18 -0
  534. package/src/registry/oauth/types.ts +96 -0
  535. package/src/registry/oauth/wafer.ts +24 -0
  536. package/src/registry/oauth/xai-oauth.ts +559 -0
  537. package/src/registry/oauth/xiaomi.ts +211 -0
  538. package/src/registry/oauth/zai.ts +285 -0
  539. package/src/registry/ollama-cloud.ts +36 -0
  540. package/src/registry/ollama.ts +43 -0
  541. package/src/registry/openai-codex-device.ts +18 -0
  542. package/src/registry/openai-codex.ts +19 -0
  543. package/src/registry/openai.ts +6 -0
  544. package/src/registry/opencode-go.ts +12 -0
  545. package/src/registry/opencode-zen.ts +12 -0
  546. package/src/registry/openrouter.ts +28 -0
  547. package/src/registry/parallel.ts +45 -0
  548. package/src/registry/perplexity.ts +13 -0
  549. package/src/registry/qianfan.ts +27 -0
  550. package/src/registry/qwen-portal.ts +50 -0
  551. package/src/registry/registry.ts +182 -0
  552. package/src/registry/sakana.ts +22 -0
  553. package/src/registry/siliconflow-cn.ts +22 -0
  554. package/src/registry/siliconflow.ts +22 -0
  555. package/src/registry/synthetic.ts +21 -0
  556. package/src/registry/tavily.ts +45 -0
  557. package/src/registry/together.ts +22 -0
  558. package/src/registry/types.ts +86 -0
  559. package/src/registry/umans.ts +23 -0
  560. package/src/registry/venice.ts +33 -0
  561. package/src/registry/vercel-ai-gateway.ts +38 -0
  562. package/src/registry/vllm.ts +34 -0
  563. package/src/registry/wafer-serverless.ts +12 -0
  564. package/src/registry/xai-oauth.ts +17 -0
  565. package/src/registry/xai.ts +22 -0
  566. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  567. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  568. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  569. package/src/registry/xiaomi.ts +12 -0
  570. package/src/registry/zai.ts +41 -0
  571. package/src/registry/zenmux.ts +22 -0
  572. package/src/registry/zhipu-coding-plan.ts +27 -0
  573. package/src/stream.ts +1944 -0
  574. package/src/types.ts +1243 -0
  575. package/src/usage/alibaba-token-plan.ts +230 -0
  576. package/src/usage/claude.ts +830 -0
  577. package/src/usage/cursor.ts +335 -0
  578. package/src/usage/gemini.ts +258 -0
  579. package/src/usage/github-copilot.ts +424 -0
  580. package/src/usage/google-antigravity.ts +497 -0
  581. package/src/usage/kimi.ts +277 -0
  582. package/src/usage/minimax-code.ts +291 -0
  583. package/src/usage/ollama.ts +41 -0
  584. package/src/usage/openai-codex-base-url.ts +35 -0
  585. package/src/usage/openai-codex-reset.ts +205 -0
  586. package/src/usage/openai-codex.ts +627 -0
  587. package/src/usage/opencode-go.ts +89 -0
  588. package/src/usage/shared.ts +10 -0
  589. package/src/usage/synthetic.ts +180 -0
  590. package/src/usage/umans.ts +192 -0
  591. package/src/usage/xai-oauth.ts +414 -0
  592. package/src/usage/zai.ts +370 -0
  593. package/src/usage.ts +411 -0
  594. package/src/utils/abort.ts +67 -0
  595. package/src/utils/anthropic-auth.ts +93 -0
  596. package/src/utils/aws-profile.ts +88 -0
  597. package/src/utils/block-symbols.ts +78 -0
  598. package/src/utils/deterministic-id.ts +20 -0
  599. package/src/utils/empty-completion-retry.ts +161 -0
  600. package/src/utils/event-stream.ts +202 -0
  601. package/src/utils/foundry.ts +8 -0
  602. package/src/utils/google-validation.ts +25 -0
  603. package/src/utils/harmony-leak.ts +500 -0
  604. package/src/utils/http-inspector.ts +196 -0
  605. package/src/utils/idle-iterator.ts +531 -0
  606. package/src/utils/leaked-thinking-stream.ts +483 -0
  607. package/src/utils/openai-http.ts +119 -0
  608. package/src/utils/openrouter-headers.ts +12 -0
  609. package/src/utils/parse-bind.ts +56 -0
  610. package/src/utils/provider-response.ts +30 -0
  611. package/src/utils/proxy.ts +314 -0
  612. package/src/utils/request-debug.ts +351 -0
  613. package/src/utils/retry-after.ts +121 -0
  614. package/src/utils/retry.ts +77 -0
  615. package/src/utils/schema/CONSTRAINTS.md +168 -0
  616. package/src/utils/schema/adapt.ts +36 -0
  617. package/src/utils/schema/compatibility.ts +435 -0
  618. package/src/utils/schema/dereference.ts +98 -0
  619. package/src/utils/schema/draft.ts +341 -0
  620. package/src/utils/schema/equality.ts +97 -0
  621. package/src/utils/schema/fields.ts +210 -0
  622. package/src/utils/schema/index.ts +14 -0
  623. package/src/utils/schema/json-schema-validator.ts +595 -0
  624. package/src/utils/schema/meta-validator.ts +167 -0
  625. package/src/utils/schema/normalize.ts +2314 -0
  626. package/src/utils/schema/spill.ts +43 -0
  627. package/src/utils/schema/stamps.ts +109 -0
  628. package/src/utils/schema/strict-tool-validation.ts +117 -0
  629. package/src/utils/schema/types.ts +10 -0
  630. package/src/utils/schema/typescript.ts +212 -0
  631. package/src/utils/schema/wire.ts +662 -0
  632. package/src/utils/sdk-stream-timeout.ts +43 -0
  633. package/src/utils/sse-debug.ts +18 -0
  634. package/src/utils/stream-markup-healing.ts +247 -0
  635. package/src/utils/thinking-loop.ts +552 -0
  636. package/src/utils/tool-call-loop-guard.ts +107 -0
  637. package/src/utils/tool-choice.ts +101 -0
  638. package/src/utils/validation.ts +1932 -0
  639. package/src/utils.ts +492 -0
package/src/stream.ts ADDED
@@ -0,0 +1,1944 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fsSync from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { scheduler } from "node:timers/promises";
6
+ import { isOfficialAnthropicApiUrl } from "@linxiraos/pi-catalog/compat/anthropic";
7
+ import type { Effort } from "@linxiraos/pi-catalog/effort";
8
+ import {
9
+ isVertexExpressOpenAIUrl,
10
+ isVertexRawPredictUrl,
11
+ resolveVertexEndpointHost,
12
+ } from "@linxiraos/pi-catalog/hosts";
13
+ import {
14
+ mapEffortToAnthropicAdaptiveEffort,
15
+ mapEffortToGoogleThinkingLevel,
16
+ minimumSupportedEffort,
17
+ requireSupportedEffort,
18
+ resolveWireModelId,
19
+ } from "@linxiraos/pi-catalog/model-thinking";
20
+ import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@linxiraos/pi-catalog/provider-models";
21
+ import { CODEX_BASE_URL } from "@linxiraos/pi-catalog/wire/codex";
22
+ import { $env, $pickenv, getProviderInFlightRoot, isEnoent, logger, withExtraCaFetch } from "@linxiraos/pi-utils";
23
+ import { getCustomApi } from "./api-registry";
24
+ import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry";
25
+ import * as AIError from "./error";
26
+ import { ProviderHttpError } from "./error";
27
+ import { isInvalidatedOAuthTokenError } from "./error/auth-classify";
28
+ import { isConcurrencyCapExclusion, isUsageLimitOutcome } from "./error/rate-limit";
29
+ import type { BedrockOptions } from "./providers/amazon-bedrock";
30
+ import type { AnthropicOptions } from "./providers/anthropic";
31
+ import { coworkFetch } from "./providers/cowork-fetch";
32
+ import type { CursorOptions } from "./providers/cursor";
33
+ import type { DevinOptions } from "./providers/devin";
34
+ import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
35
+ import { type GitLabDuoWorkflowOptions, streamGitLabDuoWorkflow } from "./providers/gitlab-duo-workflow";
36
+ import type { GoogleOptions } from "./providers/google";
37
+ import { getVertexAccessToken } from "./providers/google-auth";
38
+ import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
39
+ import type { GoogleVertexOptions } from "./providers/google-vertex";
40
+ import { isKimiModel, streamKimi } from "./providers/kimi";
41
+ import type { OllamaChatOptions } from "./providers/ollama";
42
+ import type { OpenAICompletionsOptions } from "./providers/openai-completions";
43
+ import { streamPiNative } from "./providers/pi-native-client";
44
+ // Heavy provider stream functions are imported lazily via register-builtins,
45
+ // which wraps each provider module in a dynamic import. This keeps the
46
+ // AWS SDK, google-auth-library, @google/genai, @bufbuild/protobuf, and
47
+ // other provider SDKs out of the CLI startup parse graph. The
48
+ // gitlab-duo / kimi / synthetic providers stay eager because their modules
49
+ // export routing predicates (isGitLabDuoModel, isKimiModel, isSyntheticModel)
50
+ // that must be callable synchronously before streaming begins, and their
51
+ // modules are thin wrappers with no heavy SDK dependencies.
52
+ import {
53
+ streamAnthropic,
54
+ streamAzureOpenAIResponses,
55
+ streamBedrock,
56
+ streamCursor,
57
+ streamDevin,
58
+ streamGoogle,
59
+ streamGoogleGeminiCli,
60
+ streamGoogleVertex,
61
+ streamOllama,
62
+ streamOpenAICodexResponses,
63
+ streamOpenAICompletions,
64
+ streamOpenAIResponses,
65
+ } from "./providers/register-builtins";
66
+ import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
67
+ import { getProviderDefinition, PROVIDER_REGISTRY } from "./registry";
68
+ import type {
69
+ Api,
70
+ AssistantMessage,
71
+ AssistantMessageEvent,
72
+ Context,
73
+ FetchImpl,
74
+ Model,
75
+ OptionsForApi,
76
+ SimpleStreamOptions,
77
+ StreamOptions,
78
+ ThinkingBudgets,
79
+ ToolChoice,
80
+ } from "./types";
81
+ import { resolveCacheRetention } from "./utils";
82
+ import { AssistantMessageEventStream } from "./utils/event-stream";
83
+ import { isFoundryEnabled } from "./utils/foundry";
84
+ import { wrapLeakedThinkingStream } from "./utils/leaked-thinking-stream";
85
+ import { wrapFetchForProxy } from "./utils/proxy";
86
+ import { withRequestDebugFetch } from "./utils/request-debug";
87
+ import { withGeminiThinkingLoopGuard } from "./utils/thinking-loop";
88
+
89
+ function defaultFetchForModel(model: Model<Api>): FetchImpl {
90
+ if (model.provider === "anthropic" && model.api === "anthropic-messages") return coworkFetch;
91
+ return globalThis.fetch;
92
+ }
93
+
94
+ function isGoogleVertexAuthenticatedModel(model: Model<Api>): boolean {
95
+ return (
96
+ model.provider === "google-vertex" &&
97
+ ((model.api === "openai-completions" && isVertexExpressOpenAIUrl(model.baseUrl)) ||
98
+ (model.api === "anthropic-messages" && isVertexRawPredictUrl(model.baseUrl)))
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Whether {@link model} is an official first-party endpoint whose stream needs
104
+ * no leaked-thinking healing — the official Anthropic API and the official
105
+ * OpenAI / OpenAI-Codex endpoints return structured thinking blocks and never
106
+ * leak reasoning idioms into the visible text channel.
107
+ *
108
+ * The gate is provider id **and** official endpoint URL: pointing
109
+ * `provider: "anthropic"` (or `openai`) at a custom proxy via `models.yml`
110
+ * still routes through {@link wrapLeakedThinkingStream}, since a third-party
111
+ * gateway may well leak. URL checks are strict (exact origin / path boundary
112
+ * or parsed hostname) — a substring match would accept lookalikes like
113
+ * `https://api.openai.com.evil/`. Anthropic Foundry (`CLAUDE_CODE_USE_FOUNDRY`)
114
+ * redirects an empty `baseUrl` to `FOUNDRY_BASE_URL`, so the check runs against
115
+ * that effective endpoint — exempt only when it resolves to the official host.
116
+ */
117
+ function isLeakedThinkingHealExempt(model: Model<Api>): boolean {
118
+ switch (model.provider) {
119
+ case "anthropic": {
120
+ // Mirror resolveAnthropicBaseUrl's effective endpoint: Foundry redirects
121
+ // an empty baseUrl to FOUNDRY_BASE_URL; otherwise an explicit non-official
122
+ // model.baseUrl wins, then the ANTHROPIC_BASE_URL gateway fallback, then
123
+ // the official default. Exempt only when the effective endpoint is official.
124
+ if (isFoundryEnabled()) {
125
+ const foundry = $env.FOUNDRY_BASE_URL?.trim();
126
+ if (foundry) return isOfficialAnthropicApiUrl(foundry);
127
+ }
128
+ if (model.baseUrl && !isOfficialAnthropicApiUrl(model.baseUrl)) return false;
129
+ return isOfficialAnthropicApiUrl($env.ANTHROPIC_BASE_URL?.trim() || model.baseUrl);
130
+ }
131
+ case "openai":
132
+ return isOfficialOpenAIApiUrl(model.baseUrl);
133
+ case "openai-codex":
134
+ return isOfficialCodexApiUrl(model.baseUrl);
135
+ default:
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /** Strict official-OpenAI endpoint check; missing baseUrl defaults to `api.openai.com`. */
141
+ function isOfficialOpenAIApiUrl(baseUrl: string | undefined): boolean {
142
+ if (!baseUrl) return true;
143
+ try {
144
+ return new URL(baseUrl).hostname === "api.openai.com";
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /** Strict official-Codex endpoint check; exact origin or a path boundary after {@link CODEX_BASE_URL}. */
151
+ export function isOfficialCodexApiUrl(baseUrl: string | undefined): boolean {
152
+ if (!baseUrl) return true;
153
+ const lower = baseUrl.toLowerCase().replace(/\/+$/, "");
154
+ return lower === CODEX_BASE_URL || lower.startsWith(`${CODEX_BASE_URL}/`);
155
+ }
156
+
157
+ /**
158
+ * Apply live leaked-thinking healing unless {@link model} is an official
159
+ * first-party endpoint ({@link isLeakedThinkingHealExempt}), which emits
160
+ * structured thinking and needs no healing.
161
+ */
162
+ function healLeakedThinking(model: Model<Api>, inner: AssistantMessageEventStream): AssistantMessageEventStream {
163
+ return isLeakedThinkingHealExempt(model) ? inner : wrapLeakedThinkingStream(inner);
164
+ }
165
+
166
+ type ProviderInFlightLease = {
167
+ path: string;
168
+ heartbeat: NodeJS.Timeout;
169
+ flushHeartbeat: () => Promise<void>;
170
+ };
171
+
172
+ type ProviderInFlightLeaseInfo = {
173
+ pid: number;
174
+ timestamp: number;
175
+ token: string;
176
+ };
177
+ type ProviderInFlightStaleLock = { token: string } | { mtimeMs: number };
178
+ type ProviderInFlightLockIdentity = { dev: number; ino: number; birthtimeMs: number };
179
+
180
+ const PROVIDER_INFLIGHT_LOCK_STALE_MS = 10_000;
181
+ const PROVIDER_INFLIGHT_LEASE_STALE_MS = 30_000;
182
+ const PROVIDER_INFLIGHT_HEARTBEAT_MS = 5_000;
183
+ const PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS = 250;
184
+
185
+ let configuredProviderMaxInFlightRequests: Record<string, number> = {};
186
+ let providerInFlightRootOverride: string | undefined;
187
+
188
+ export function configureProviderMaxInFlightRequests(limits: Record<string, number> | undefined): void {
189
+ configuredProviderMaxInFlightRequests = limits ?? {};
190
+ }
191
+
192
+ function resolveProviderInFlightLimit(
193
+ provider: string,
194
+ options?: Pick<StreamOptions, "maxInFlightRequests">,
195
+ ): number | undefined {
196
+ const limits = options?.maxInFlightRequests ?? configuredProviderMaxInFlightRequests;
197
+ const value = limits[provider];
198
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined;
199
+ return Math.max(1, Math.floor(value));
200
+ }
201
+
202
+ function providerInFlightRoot(): string {
203
+ if (providerInFlightRootOverride) return providerInFlightRootOverride;
204
+ return getProviderInFlightRoot();
205
+ }
206
+
207
+ function providerInFlightSegment(provider: string): string {
208
+ return crypto.createHash("sha256").update(provider).digest("base64url");
209
+ }
210
+
211
+ function providerInFlightDir(provider: string): string {
212
+ return path.join(providerInFlightRoot(), providerInFlightSegment(provider));
213
+ }
214
+
215
+ function providerInFlightSignalPath(provider: string): string {
216
+ return path.join(providerInFlightDir(provider), ".wakeup");
217
+ }
218
+
219
+ function providerInFlightLockDir(provider: string): string {
220
+ return `${providerInFlightDir(provider)}.lock`;
221
+ }
222
+
223
+ // `process.kill(pid, 0)` may throw for permission/sandbox reasons even when a
224
+ // process exists. Treat non-ESRCH failures as alive; timestamp expiry still
225
+ // reaps leases whose heartbeat stopped.
226
+ function isProcessAlive(pid: number): boolean {
227
+ try {
228
+ process.kill(pid, 0);
229
+ return true;
230
+ } catch (error) {
231
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
232
+ }
233
+ }
234
+
235
+ async function readProviderInFlightInfo(infoPath: string): Promise<ProviderInFlightLeaseInfo | null> {
236
+ try {
237
+ const content = await fs.readFile(infoPath, "utf-8");
238
+ const parsed = JSON.parse(content) as Partial<ProviderInFlightLeaseInfo>;
239
+ if (typeof parsed.pid !== "number" || typeof parsed.timestamp !== "number" || typeof parsed.token !== "string") {
240
+ return null;
241
+ }
242
+ return { pid: parsed.pid, timestamp: parsed.timestamp, token: parsed.token };
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ async function writeProviderInFlightInfo(dir: string, token: string): Promise<void> {
249
+ const info: ProviderInFlightLeaseInfo = { pid: process.pid, timestamp: Date.now(), token };
250
+ const infoPath = path.join(dir, "info.json");
251
+ const tempPath = path.join(dir, `.info-${process.pid}-${crypto.randomUUID()}.tmp`);
252
+ try {
253
+ await Bun.write(tempPath, JSON.stringify(info));
254
+ await fs.rename(tempPath, infoPath);
255
+ } catch (error) {
256
+ await fs.rm(tempPath, { force: true }).catch(() => {});
257
+ throw error;
258
+ }
259
+ }
260
+
261
+ async function isProviderInFlightDirStale(dir: string, staleMs: number): Promise<boolean> {
262
+ const info = await readProviderInFlightInfo(path.join(dir, "info.json"));
263
+ if (info) {
264
+ if (!isProcessAlive(info.pid)) return true;
265
+ return Date.now() - info.timestamp > staleMs;
266
+ }
267
+
268
+ try {
269
+ const stat = await fs.stat(path.join(dir, "info.json"));
270
+ return Date.now() - stat.mtimeMs > staleMs;
271
+ } catch (error) {
272
+ if (!isEnoent(error)) throw error;
273
+ }
274
+
275
+ try {
276
+ const stat = await fs.stat(dir);
277
+ return Date.now() - stat.mtimeMs > staleMs;
278
+ } catch (error) {
279
+ if (isEnoent(error)) return false;
280
+ throw error;
281
+ }
282
+ }
283
+
284
+ async function readProviderInFlightStaleLock(lockDir: string): Promise<ProviderInFlightStaleLock | null> {
285
+ const infoPath = path.join(lockDir, "info.json");
286
+ const info = await readProviderInFlightInfo(infoPath);
287
+ if (info) return isProcessAlive(info.pid) ? null : { token: info.token };
288
+
289
+ try {
290
+ const stat = await fs.stat(lockDir);
291
+ return Date.now() - stat.mtimeMs > PROVIDER_INFLIGHT_LOCK_STALE_MS ? { mtimeMs: stat.mtimeMs } : null;
292
+ } catch (error) {
293
+ if (isEnoent(error)) return null;
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ async function readProviderInFlightLockIdentity(lockDir: string): Promise<ProviderInFlightLockIdentity> {
299
+ const stat = await fs.stat(lockDir);
300
+ return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
301
+ }
302
+
303
+ function isSameProviderInFlightLock(
304
+ current: ProviderInFlightLockIdentity,
305
+ expected: ProviderInFlightLockIdentity,
306
+ ): boolean {
307
+ if (current.dev !== expected.dev) return false;
308
+ if (current.ino !== 0 || expected.ino !== 0) return current.ino === expected.ino;
309
+ return current.birthtimeMs === expected.birthtimeMs;
310
+ }
311
+
312
+ async function releaseProviderInFlightStaleLock(lockDir: string, stale: ProviderInFlightStaleLock): Promise<void> {
313
+ if ("token" in stale) {
314
+ await releaseProviderInFlightLock(lockDir, stale.token);
315
+ return;
316
+ }
317
+
318
+ const infoPath = path.join(lockDir, "info.json");
319
+ if (await readProviderInFlightInfo(infoPath)) return;
320
+ try {
321
+ const stat = await fs.stat(lockDir);
322
+ if (stat.mtimeMs !== stale.mtimeMs || Date.now() - stat.mtimeMs <= PROVIDER_INFLIGHT_LOCK_STALE_MS) return;
323
+ await fs.rm(lockDir, { recursive: true, force: true });
324
+ } catch {}
325
+ }
326
+
327
+ // Best-effort token-checked release. A token mismatch means another process has
328
+ // already replaced the lock, so the fresh lock must be left intact.
329
+ async function releaseProviderInFlightLock(lockDir: string, token: string): Promise<void> {
330
+ try {
331
+ const info = await readProviderInFlightInfo(path.join(lockDir, "info.json"));
332
+ if (!info || info.token !== token) return;
333
+ await fs.rm(lockDir, { recursive: true, force: true });
334
+ } catch {}
335
+ }
336
+
337
+ async function releaseProviderInFlightLockDirIfSame(
338
+ lockDir: string,
339
+ identity: ProviderInFlightLockIdentity,
340
+ ): Promise<void> {
341
+ try {
342
+ if (await readProviderInFlightInfo(path.join(lockDir, "info.json"))) return;
343
+ const current = await readProviderInFlightLockIdentity(lockDir);
344
+ if (!isSameProviderInFlightLock(current, identity)) return;
345
+ await fs.rm(lockDir, { recursive: true, force: true });
346
+ } catch {}
347
+ }
348
+
349
+ async function acquireProviderInFlightLock(provider: string, signal?: AbortSignal): Promise<() => Promise<void>> {
350
+ const lockDir = providerInFlightLockDir(provider);
351
+ await fs.mkdir(path.dirname(lockDir), { recursive: true });
352
+
353
+ while (true) {
354
+ if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
355
+ try {
356
+ await fs.mkdir(lockDir);
357
+ const lockIdentity = await readProviderInFlightLockIdentity(lockDir);
358
+ const token = crypto.randomUUID();
359
+ try {
360
+ await writeProviderInFlightInfo(lockDir, token);
361
+ } catch (error) {
362
+ await releaseProviderInFlightLockDirIfSame(lockDir, lockIdentity);
363
+ throw error;
364
+ }
365
+ return async () => {
366
+ await releaseProviderInFlightLock(lockDir, token);
367
+ };
368
+ } catch (error) {
369
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
370
+ }
371
+
372
+ const staleLock = await readProviderInFlightStaleLock(lockDir);
373
+ if (staleLock) {
374
+ await releaseProviderInFlightStaleLock(lockDir, staleLock);
375
+ await signalProviderInFlightWaiters(provider);
376
+ continue;
377
+ }
378
+
379
+ await waitForProviderInFlightSignal(provider, signal);
380
+ }
381
+ }
382
+
383
+ async function cleanupProviderInFlightLeases(providerDir: string): Promise<number> {
384
+ let active = 0;
385
+ let entries: string[];
386
+ try {
387
+ entries = await fs.readdir(providerDir);
388
+ } catch (error) {
389
+ if (isEnoent(error)) return 0;
390
+ throw error;
391
+ }
392
+
393
+ for (const entry of entries) {
394
+ const leaseDir = path.join(providerDir, entry);
395
+ let isDirectory = false;
396
+ try {
397
+ isDirectory = (await fs.stat(leaseDir)).isDirectory();
398
+ } catch (error) {
399
+ if (isEnoent(error)) continue;
400
+ throw error;
401
+ }
402
+ if (!isDirectory) continue;
403
+ if (await isProviderInFlightDirStale(leaseDir, PROVIDER_INFLIGHT_LEASE_STALE_MS)) {
404
+ await fs.rm(leaseDir, { recursive: true, force: true });
405
+ continue;
406
+ }
407
+ active++;
408
+ }
409
+ return active;
410
+ }
411
+
412
+ async function tryAcquireProviderInFlightLease(
413
+ provider: string,
414
+ limit: number,
415
+ signal?: AbortSignal,
416
+ ): Promise<ProviderInFlightLease | null> {
417
+ const releaseLock = await acquireProviderInFlightLock(provider, signal);
418
+ try {
419
+ const dir = providerInFlightDir(provider);
420
+ await fs.mkdir(dir, { recursive: true });
421
+ const active = await cleanupProviderInFlightLeases(dir);
422
+ if (active >= limit) return null;
423
+
424
+ const leaseDir = path.join(dir, `${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
425
+ const token = crypto.randomUUID();
426
+ try {
427
+ await fs.mkdir(leaseDir);
428
+ await writeProviderInFlightInfo(leaseDir, token);
429
+ } catch (error) {
430
+ await removeProviderInFlightLeaseDir(leaseDir).catch(() => {});
431
+ throw error;
432
+ }
433
+ let heartbeatFlush = Promise.resolve();
434
+ const touchHeartbeat = () => {
435
+ heartbeatFlush = heartbeatFlush
436
+ .then(
437
+ () => writeProviderInFlightInfo(leaseDir, token),
438
+ () => writeProviderInFlightInfo(leaseDir, token),
439
+ )
440
+ .catch(() => {});
441
+ };
442
+ const heartbeat = setInterval(touchHeartbeat, PROVIDER_INFLIGHT_HEARTBEAT_MS);
443
+ heartbeat.unref?.();
444
+ return { path: leaseDir, heartbeat, flushHeartbeat: () => heartbeatFlush };
445
+ } finally {
446
+ await releaseLock();
447
+ }
448
+ }
449
+
450
+ async function signalProviderInFlightWaitersInDir(dir: string): Promise<void> {
451
+ try {
452
+ await fs.mkdir(dir, { recursive: true });
453
+ await Bun.write(path.join(dir, ".wakeup"), String(Date.now()));
454
+ } catch {}
455
+ }
456
+
457
+ async function signalProviderInFlightWaiters(provider: string): Promise<void> {
458
+ await signalProviderInFlightWaitersInDir(providerInFlightDir(provider));
459
+ }
460
+
461
+ function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal): Promise<void> {
462
+ if (signal?.aborted)
463
+ return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
464
+ const signalPath = providerInFlightSignalPath(provider);
465
+ const waitStarted = Date.now();
466
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
467
+ let settled = false;
468
+ let watcher: fsSync.FSWatcher | undefined;
469
+ const timer = setTimeout(() => finish(resolve), PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS);
470
+ const finish = (settle: () => void) => {
471
+ if (settled) return;
472
+ settled = true;
473
+ clearTimeout(timer);
474
+ watcher?.close();
475
+ signal?.removeEventListener("abort", onAbort);
476
+ settle();
477
+ };
478
+ const onAbort = () => {
479
+ finish(() => reject(signal?.reason ?? new AIError.AbortError("Provider request aborted before dispatch")));
480
+ };
481
+ signal?.addEventListener("abort", onAbort, { once: true });
482
+ try {
483
+ watcher = fsSync.watch(providerInFlightDir(provider), (_event, filename) => {
484
+ if (filename === ".wakeup" || filename === null) {
485
+ finish(resolve);
486
+ }
487
+ });
488
+ void fs.stat(signalPath).then(
489
+ stat => {
490
+ if (stat.mtimeMs >= waitStarted) finish(resolve);
491
+ },
492
+ error => {
493
+ if (!isEnoent(error)) finish(resolve);
494
+ },
495
+ );
496
+ } catch {
497
+ // Filesystem notifications are best-effort across platforms; the fallback
498
+ // timer keeps stale-lock/lease cleanup progressing if an event is dropped.
499
+ }
500
+ return promise;
501
+ }
502
+
503
+ async function removeProviderInFlightLeaseDir(leasePath: string): Promise<void> {
504
+ for (let attempt = 0; attempt < 3; attempt++) {
505
+ try {
506
+ await fs.rm(leasePath, { recursive: true, force: true });
507
+ return;
508
+ } catch (error) {
509
+ if (isEnoent(error)) return;
510
+ const code = (error as NodeJS.ErrnoException).code;
511
+ if (attempt < 2 && (code === "EBUSY" || code === "ENOTEMPTY" || code === "EPERM")) {
512
+ await Bun.sleep(25);
513
+ continue;
514
+ }
515
+ throw error;
516
+ }
517
+ }
518
+ }
519
+
520
+ // Signal into the lease's OWN provider directory (derived from `lease.path`)
521
+ // rather than recomputing it from the current root. A release that lands after
522
+ // the in-flight root has been repointed (only the test seam does that) must not
523
+ // write `.wakeup` into an unrelated provider directory.
524
+ async function releaseProviderInFlightLease(lease: ProviderInFlightLease): Promise<void> {
525
+ clearInterval(lease.heartbeat);
526
+ await lease.flushHeartbeat();
527
+ await removeProviderInFlightLeaseDir(lease.path);
528
+ await signalProviderInFlightWaitersInDir(path.dirname(lease.path));
529
+ }
530
+
531
+ async function acquireProviderInFlightSlot(
532
+ provider: string,
533
+ limit: number | undefined,
534
+ signal?: AbortSignal,
535
+ ): Promise<() => Promise<void>> {
536
+ if (limit === undefined) return async () => {};
537
+ let loggedWait = false;
538
+ while (true) {
539
+ if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
540
+ const lease = await tryAcquireProviderInFlightLease(provider, limit, signal);
541
+ if (lease) return () => releaseProviderInFlightLease(lease);
542
+ if (!loggedWait) {
543
+ loggedWait = true;
544
+ logger.debug("Provider in-flight limit blocked request", { provider, limit });
545
+ }
546
+ await waitForProviderInFlightSignal(provider, signal);
547
+ }
548
+ }
549
+
550
+ export const __providerInFlightForTesting = {
551
+ setRoot(root: string | undefined): void {
552
+ providerInFlightRootOverride = root;
553
+ },
554
+ providerDir(provider: string): string {
555
+ return providerInFlightDir(provider);
556
+ },
557
+ lockDir(provider: string): string {
558
+ return providerInFlightLockDir(provider);
559
+ },
560
+ async captureStaleLockRelease(provider: string): Promise<(() => Promise<void>) | null> {
561
+ const lockDir = providerInFlightLockDir(provider);
562
+ const stale = await readProviderInFlightStaleLock(lockDir);
563
+ if (!stale) return null;
564
+ return () => releaseProviderInFlightStaleLock(lockDir, stale);
565
+ },
566
+ async captureLockDirRelease(provider: string): Promise<(() => Promise<void>) | null> {
567
+ const lockDir = providerInFlightLockDir(provider);
568
+ try {
569
+ const identity = await readProviderInFlightLockIdentity(lockDir);
570
+ return () => releaseProviderInFlightLockDirIfSame(lockDir, identity);
571
+ } catch {
572
+ return null;
573
+ }
574
+ },
575
+ };
576
+
577
+ function withProviderInFlightLimit<TOptions extends Pick<StreamOptions, "signal" | "maxInFlightRequests">>(
578
+ model: Model<Api>,
579
+ options: TOptions | undefined,
580
+ dispatch: () => AssistantMessageEventStream,
581
+ ): AssistantMessageEventStream {
582
+ // Leaked-thinking healing folds in here — the one shared provider-dispatch
583
+ // chokepoint — so the loop guard (which wraps this) sees healed events and all
584
+ // provider exits are covered by one wrap. Official first-party providers are
585
+ // exempt (see `healLeakedThinking`); healing is otherwise idempotent.
586
+ const limit = resolveProviderInFlightLimit(model.provider, options);
587
+ if (limit === undefined) return healLeakedThinking(model, dispatch());
588
+
589
+ const outer = new AssistantMessageEventStream();
590
+ void (async () => {
591
+ let release: (() => Promise<void>) | undefined;
592
+ let released = false;
593
+ const releaseOnce = async () => {
594
+ if (!release || released) return;
595
+ released = true;
596
+ await release();
597
+ };
598
+ try {
599
+ const startedWaitingAt = Date.now();
600
+ release = await acquireProviderInFlightSlot(model.provider, limit, options?.signal);
601
+ if (Date.now() - startedWaitingAt >= PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS) {
602
+ logger.debug("Provider in-flight limit wait completed", { provider: model.provider, limit });
603
+ }
604
+ if (options?.signal?.aborted) {
605
+ throw options.signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
606
+ }
607
+ const inner = healLeakedThinking(model, dispatch());
608
+ try {
609
+ for await (const event of inner) {
610
+ outer.push(event);
611
+ if (outer.done) return;
612
+ }
613
+ if (!outer.done) outer.end(await inner.result());
614
+ } finally {
615
+ await releaseOnce();
616
+ }
617
+ } catch (error) {
618
+ await releaseOnce();
619
+ if (!outer.done) outer.fail(error);
620
+ }
621
+ })();
622
+ return outer;
623
+ }
624
+
625
+ function createVertexAuthenticatedFetch(options: StreamOptions | undefined): FetchImpl {
626
+ const baseFetch = options?.fetch ?? fetch;
627
+ const vertexFetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
628
+ const token = await getVertexAccessToken({ signal: options?.signal, fetch: baseFetch });
629
+ const headers = new Headers(init?.headers);
630
+ headers.set("Authorization", `Bearer ${token}`);
631
+ const rewritten = resolveVertexRequest(input);
632
+ const url = rewritten instanceof Request ? rewritten.url : rewritten.toString();
633
+ if (isVertexRawPredictUrl(url)) {
634
+ const bodyText = await readVertexRequestBody(rewritten, init);
635
+ const transformed = transformVertexAnthropicBody(bodyText);
636
+ return baseFetch(url, {
637
+ ...init,
638
+ method: init?.method ?? (rewritten instanceof Request ? rewritten.method : "POST"),
639
+ headers,
640
+ body: transformed,
641
+ });
642
+ }
643
+ return baseFetch(rewritten, { ...init, headers });
644
+ };
645
+ return Object.assign(vertexFetch, baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {});
646
+ }
647
+
648
+ async function readVertexRequestBody(input: string | URL | Request, init: RequestInit | undefined): Promise<string> {
649
+ if (input instanceof Request) return input.clone().text();
650
+ const body = init?.body;
651
+ if (typeof body === "string") return body;
652
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
653
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
654
+ return "";
655
+ }
656
+
657
+ // Vertex Claude rejects the standard Anthropic body shape: the `model` field
658
+ // is encoded in the URL path and `anthropic_version: "vertex-2023-10-16"` is
659
+ // required in the JSON body instead of the `anthropic-version` HTTP header.
660
+ function transformVertexAnthropicBody(bodyText: string): string {
661
+ if (!bodyText) return bodyText;
662
+ try {
663
+ const payload = JSON.parse(bodyText) as Record<string, unknown>;
664
+ delete payload.model;
665
+ payload.anthropic_version = "vertex-2023-10-16";
666
+ return JSON.stringify(payload);
667
+ } catch {
668
+ return bodyText;
669
+ }
670
+ }
671
+
672
+ function resolveVertexRequest(input: string | URL | Request): string | URL | Request {
673
+ const project = $env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT;
674
+ const location = $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
675
+ if (!project || !location) return input;
676
+
677
+ const rewriteUrl = (url: string): string => {
678
+ const hasPlaceholder =
679
+ url.includes("{project}") ||
680
+ url.includes("{location}") ||
681
+ url.includes("%7Bproject%7D") ||
682
+ url.includes("%7Blocation%7D");
683
+ const host = resolveVertexEndpointHost(location);
684
+ const rewritten = hasPlaceholder
685
+ ? url
686
+ .replace("https://{location}-aiplatform.googleapis.com", `https://${host}`)
687
+ .replace("https://%7Blocation%7D-aiplatform.googleapis.com", `https://${host}`)
688
+ .replaceAll("{project}", encodeURIComponent(project))
689
+ .replaceAll("%7Bproject%7D", encodeURIComponent(project))
690
+ .replaceAll("{location}", encodeURIComponent(location))
691
+ .replaceAll("%7Blocation%7D", encodeURIComponent(location))
692
+ : url;
693
+ return rewritten.replace(":streamRawPredict/v1/messages", ":streamRawPredict");
694
+ };
695
+
696
+ if (input instanceof Request) {
697
+ const rewrittenUrl = rewriteUrl(input.url);
698
+ return rewrittenUrl === input.url ? input : new Request(rewrittenUrl, input);
699
+ }
700
+ if (input instanceof URL) {
701
+ const rewrittenUrl = rewriteUrl(input.toString());
702
+ return rewrittenUrl === input.toString() ? input : new URL(rewrittenUrl);
703
+ }
704
+ return rewriteUrl(input);
705
+ }
706
+
707
+ type KeyResolver = string | (() => string | undefined);
708
+
709
+ const LEGACY_ENV_KEYS: Record<string, KeyResolver> = {
710
+ // Non-provider / search-tool keys and API-name keys not modeled as registry provider defs.
711
+ "azure-openai-responses": "AZURE_OPENAI_API_KEY",
712
+ jina: "JINA_API_KEY",
713
+ brave: "BRAVE_API_KEY",
714
+ tinyfish: "TINYFISH_API_KEY",
715
+ firecrawl: "FIRECRAWL_API_KEY",
716
+ };
717
+
718
+ /**
719
+ * Env fallbacks derived from the catalog table — the single source for plain
720
+ * provider env-var names. Registry defs override with computed resolvers
721
+ * (Foundry/ADC/Bedrock probes); legacy non-provider keys merge last.
722
+ */
723
+ const CATALOG_ENTRY_ENV_KEYS = (CATALOG_PROVIDERS as readonly ProviderCatalogEntry[]).flatMap(provider => {
724
+ const envVars = provider.envVars;
725
+ if (!envVars || envVars.length === 0) return [];
726
+ const resolver: KeyResolver = envVars.length === 1 ? envVars[0] : () => $pickenv(...envVars);
727
+ return [[provider.id, resolver] as [string, KeyResolver]];
728
+ });
729
+
730
+ const serviceProviderMap: Record<string, KeyResolver> = {
731
+ ...Object.fromEntries(CATALOG_ENTRY_ENV_KEYS),
732
+ ...Object.fromEntries(
733
+ PROVIDER_REGISTRY.flatMap(provider =>
734
+ provider.envKeys != null ? [[provider.id, provider.envKeys] as [string, KeyResolver]] : [],
735
+ ),
736
+ ),
737
+ ...LEGACY_ENV_KEYS,
738
+ };
739
+
740
+ /**
741
+ * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
742
+ *
743
+ * Will not return API keys for providers that require OAuth tokens.
744
+ * Checks Bun.env, then cwd/.env, then ~/.env.
745
+ */
746
+ export function getEnvApiKey(provider: string): string | undefined {
747
+ const resolver = serviceProviderMap[provider];
748
+ if (typeof resolver === "string") {
749
+ return $env[resolver];
750
+ }
751
+ return resolver?.();
752
+ }
753
+
754
+ /**
755
+ * Name of the environment variable that backs `getEnvApiKey` for a provider,
756
+ * when that provider maps to a single named variable (e.g. `github-copilot` →
757
+ * `COPILOT_GITHUB_TOKEN`). Returns undefined for providers whose env fallback
758
+ * is computed (multi-var pickers, Vertex ADC / Bedrock probes, …) since no
759
+ * single variable name describes the source.
760
+ */
761
+ export function getEnvApiKeyName(provider: string): string | undefined {
762
+ const resolver = serviceProviderMap[provider];
763
+ return typeof resolver === "string" ? resolver : undefined;
764
+ }
765
+
766
+ /**
767
+ * Enumerate every provider that has an env-var fallback for `getEnvApiKey`.
768
+ * Used by `omp auth-broker migrate --include-env` to discover env-sourced keys
769
+ * that should be uploaded to the broker.
770
+ */
771
+ export function listProvidersWithEnvKey(): string[] {
772
+ return Object.keys(serviceProviderMap);
773
+ }
774
+
775
+ export function stream<TApi extends Api>(
776
+ model: Model<TApi>,
777
+ context: Context,
778
+ options?: OptionsForApi<TApi>,
779
+ ): AssistantMessageEventStream {
780
+ return withGeminiThinkingLoopGuard(model, options, opts =>
781
+ withProviderInFlightLimit(model, opts, () => streamDispatch(model, context, opts)),
782
+ );
783
+ }
784
+
785
+ function streamDispatch<TApi extends Api>(
786
+ model: Model<TApi>,
787
+ context: Context,
788
+ options?: OptionsForApi<TApi>,
789
+ ): AssistantMessageEventStream {
790
+ const inputOptions = (options || {}) as StreamOptions;
791
+ const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
792
+ const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
793
+ const requestOptions = {
794
+ ...debugOptions,
795
+ fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
796
+ } as OptionsForApi<TApi>;
797
+ assertExplicitOpenAIResponsesPromptCacheSupport(model, requestOptions);
798
+
799
+ // Check custom API registry first (extension-provided APIs like "vertex-claude-api")
800
+ const customApiProvider = getCustomApi(model.api);
801
+ if (customApiProvider) {
802
+ return customApiProvider.stream(model, context, requestOptions as StreamOptions);
803
+ }
804
+
805
+ if (isGitLabDuoModel(model)) {
806
+ const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
807
+ if (!apiKey) {
808
+ throw new AIError.MissingApiKeyError(model.provider);
809
+ }
810
+ return streamGitLabDuo(model, context, {
811
+ ...(requestOptions as SimpleStreamOptions),
812
+ apiKey,
813
+ });
814
+ }
815
+
816
+ if (model.api === "gitlab-duo-agent") {
817
+ const apiKey = (requestOptions as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
818
+ if (!apiKey) {
819
+ throw new AIError.MissingApiKeyError(model.provider);
820
+ }
821
+ return streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
822
+ ...(requestOptions as StreamOptions | undefined),
823
+ apiKey,
824
+ } as GitLabDuoWorkflowOptions);
825
+ }
826
+
827
+ // Vertex AI and Bedrock Converse authenticate outside the generic API-key path.
828
+ if (model.api === "google-vertex") {
829
+ return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
830
+ }
831
+ if (model.api === "bedrock-converse-stream") {
832
+ return streamBedrock(model as Model<"bedrock-converse-stream">, context, requestOptions as BedrockOptions);
833
+ }
834
+
835
+ const prepareRequest = getProviderDefinition(model.provider)?.prepareRequest;
836
+ const prepared = prepareRequest?.(model as Model<Api>, requestOptions as StreamOptions);
837
+ const providerModel = prepared?.model ?? (model as Model<Api>);
838
+ const preparedOptions = prepared?.options ?? (requestOptions as StreamOptions);
839
+ const apiKey = preparedOptions.apiKey || getEnvApiKey(providerModel.provider);
840
+ if (!apiKey) {
841
+ throw new AIError.MissingApiKeyError(providerModel.provider);
842
+ }
843
+ const providerOptions = isGoogleVertexAuthenticatedModel(providerModel)
844
+ ? {
845
+ ...preparedOptions,
846
+ apiKey: "vertex-adc",
847
+ fetch: createVertexAuthenticatedFetch(preparedOptions),
848
+ }
849
+ : { ...preparedOptions, apiKey };
850
+
851
+ const api: Api = providerModel.api;
852
+ switch (api) {
853
+ case "anthropic-messages": {
854
+ const anthropicOptions = providerOptions as AnthropicOptions;
855
+ return streamAnthropic(providerModel as Model<"anthropic-messages">, context, {
856
+ ...anthropicOptions,
857
+ isOAuth: anthropicOptions.isOAuth ?? providerModel.isOAuth,
858
+ });
859
+ }
860
+
861
+ case "openrouter": {
862
+ const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
863
+ if (useResponses) {
864
+ return streamOpenAIResponses(
865
+ providerModel as Model<"openai-responses">,
866
+ context,
867
+ providerOptions as OptionsForApi<"openai-responses">,
868
+ );
869
+ }
870
+ return streamOpenAICompletions(
871
+ providerModel as Model<"openai-completions">,
872
+ context,
873
+ providerOptions as OptionsForApi<"openai-completions">,
874
+ );
875
+ }
876
+
877
+ case "openai-completions":
878
+ return streamOpenAICompletions(
879
+ providerModel as Model<"openai-completions">,
880
+ context,
881
+ providerOptions as OptionsForApi<"openai-completions">,
882
+ );
883
+
884
+ case "openai-responses":
885
+ return streamOpenAIResponses(
886
+ providerModel as Model<"openai-responses">,
887
+ context,
888
+ providerOptions as OptionsForApi<"openai-responses">,
889
+ );
890
+
891
+ case "azure-openai-responses":
892
+ return streamAzureOpenAIResponses(
893
+ providerModel as Model<"azure-openai-responses">,
894
+ context,
895
+ providerOptions as OptionsForApi<"azure-openai-responses">,
896
+ );
897
+
898
+ case "openai-codex-responses":
899
+ return streamOpenAICodexResponses(
900
+ providerModel as Model<"openai-codex-responses">,
901
+ context,
902
+ providerOptions as OptionsForApi<"openai-codex-responses">,
903
+ );
904
+
905
+ case "google-generative-ai":
906
+ return streamGoogle(providerModel as Model<"google-generative-ai">, context, providerOptions);
907
+
908
+ case "google-gemini-cli":
909
+ return streamGoogleGeminiCli(
910
+ providerModel as Model<"google-gemini-cli">,
911
+ context,
912
+ providerOptions as GoogleGeminiCliOptions,
913
+ );
914
+
915
+ case "ollama-chat":
916
+ return streamOllama(providerModel as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
917
+
918
+ case "cursor-agent":
919
+ return streamCursor(providerModel as Model<"cursor-agent">, context, providerOptions as CursorOptions);
920
+
921
+ case "devin-agent":
922
+ return streamDevin(providerModel as Model<"devin-agent">, context, providerOptions as DevinOptions);
923
+
924
+ default:
925
+ throw new AIError.ConfigurationError(`Unhandled API: ${api}`);
926
+ }
927
+ }
928
+
929
+ /** Thinking-loop re-samples spent before {@link resolveWithThinkingLoopCook} cooks. */
930
+ const THINKING_LOOP_MAX_ABORTS = 3;
931
+ const THINKING_LOOP_RETRY_BASE_DELAY_MS = 500;
932
+ const THINKING_LOOP_RETRY_MAX_DELAY_MS = 8_000;
933
+
934
+ /**
935
+ * Resolve a completion, re-sampling a thinking-loop stall up to
936
+ * {@link THINKING_LOOP_MAX_ABORTS} times before letting it cook. The loop guard
937
+ * raises an empty `stopReason: "error"` stall on each guarded attempt; this
938
+ * result-path consumer re-dispatches a fresh request per stall and, once the abort
939
+ * budget is spent, runs one final pass with the guard disabled so a stubborn loop
940
+ * returns the model's raw output instead of a fatal stall. Non-stall results —
941
+ * including genuine errors — return immediately; a caller abort during backoff
942
+ * propagates so cancellation surfaces as an abort, never a stale stall result.
943
+ */
944
+ async function resolveWithThinkingLoopCook(
945
+ signal: AbortSignal | undefined,
946
+ dispatch: () => AssistantMessageEventStream,
947
+ cook: () => AssistantMessageEventStream,
948
+ ): Promise<AssistantMessage> {
949
+ let message = await dispatch().result();
950
+ let thinkingLoopRetry = AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
951
+ for (let attempt = 0; thinkingLoopRetry && attempt < THINKING_LOOP_MAX_ABORTS - 1; attempt += 1) {
952
+ // A caller abort surfaces as a thrown abort (never the stall, which would
953
+ // misclassify as a 502): throwIfAborted before backoff, and scheduler.wait
954
+ // rejects if the abort lands mid-delay.
955
+ signal?.throwIfAborted();
956
+ const delay = Math.min(THINKING_LOOP_RETRY_BASE_DELAY_MS * 2 ** attempt, THINKING_LOOP_RETRY_MAX_DELAY_MS);
957
+ await scheduler.wait(delay, { signal });
958
+ message = await dispatch().result();
959
+ thinkingLoopRetry =
960
+ message.stopReason === "error" &&
961
+ message.content.length === 0 &&
962
+ AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
963
+ }
964
+ if (!thinkingLoopRetry) return message;
965
+ signal?.throwIfAborted();
966
+ // Abort budget spent and still looping: let it cook with the guard disabled.
967
+ return cook().result();
968
+ }
969
+
970
+ export async function complete<TApi extends Api>(
971
+ model: Model<TApi>,
972
+ context: Context,
973
+ options?: OptionsForApi<TApi>,
974
+ ): Promise<AssistantMessage> {
975
+ return resolveWithThinkingLoopCook(
976
+ options?.signal,
977
+ () => stream(model, context, options),
978
+ () => stream(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
979
+ );
980
+ }
981
+
982
+ type AuthRetryFailure = {
983
+ error: unknown;
984
+ bufferedEvents: AssistantMessageEvent[];
985
+ terminalEvent?: Extract<AssistantMessageEvent, { type: "error" }>;
986
+ };
987
+
988
+ function extractStatusFromAssistantError(message: AssistantMessage): number | undefined {
989
+ if (message.errorStatus !== undefined) return message.errorStatus;
990
+ if (!message.errorMessage) return undefined;
991
+ return AIError.status({ message: message.errorMessage });
992
+ }
993
+
994
+ function isRetryableUpstreamError(error: unknown, status: number | undefined, message: string | undefined): boolean {
995
+ // 401 means the credential is bad; 403 is its valid-token twin (access
996
+ // denied by plan, model policy, or org restriction — a sibling account may
997
+ // not share it). Usage-limit phrasing (Codex's
998
+ // "You have hit your ChatGPT usage limit", Anthropic's "usage_limit_reached",
999
+ // Google's "resource_exhausted", OpenAI's "insufficient_quota") and 429s
1000
+ // without transient rate-limit wording mean this account is parked but a
1001
+ // sibling credential can usually pick the request up. Both are rotatable
1002
+ // via `onAuthError` — the auth-gateway maps the former to
1003
+ // `invalidateCredentialMatching` and the latter to
1004
+ // `markUsageLimitReached`. Transient 429s ("Too many requests",
1005
+ // per-minute caps) classify as RATE_LIMIT_EXCEEDED in
1006
+ // `parseRateLimitReason` and stay in the provider's own backoff layer
1007
+ // instead of burning siblings.
1008
+ if (AIError.isUsageLimit(error)) return true;
1009
+ if (isInvalidatedOAuthTokenError(error)) return true;
1010
+ if (status === 401 || (status === 403 && !isConcurrencyCapExclusion(status, message))) return true;
1011
+ return isUsageLimitOutcome(status, message);
1012
+ }
1013
+
1014
+ function createAssistantAuthError(message: AssistantMessage): Error {
1015
+ const text = message.errorMessage ?? "Provider authentication failed";
1016
+ const status = extractStatusFromAssistantError(message);
1017
+ const error =
1018
+ status === undefined
1019
+ ? new AIError.ProviderResponseError(text, { kind: "runtime" })
1020
+ : new ProviderHttpError(text, status);
1021
+ return typeof message.errorId === "number" ? AIError.attach(error, message.errorId) : error;
1022
+ }
1023
+
1024
+ function emitBufferedEvents(stream: AssistantMessageEventStream, events: AssistantMessageEvent[]): void {
1025
+ for (const event of events) {
1026
+ stream.push(event);
1027
+ }
1028
+ }
1029
+
1030
+ export function streamSimple<TApi extends Api>(
1031
+ model: Model<TApi>,
1032
+ context: Context,
1033
+ options?: SimpleStreamOptions,
1034
+ ): AssistantMessageEventStream {
1035
+ const inputOptions = (options || {}) as SimpleStreamOptions;
1036
+ const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
1037
+ const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
1038
+ const requestOptions = {
1039
+ ...debugOptions,
1040
+ fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
1041
+ } as SimpleStreamOptions;
1042
+
1043
+ const apiKeyResolver = isApiKeyResolver(requestOptions?.apiKey) ? requestOptions.apiKey : undefined;
1044
+ if (apiKeyResolver) {
1045
+ const outer = new AssistantMessageEventStream();
1046
+ const signal = requestOptions?.signal;
1047
+ // One inner attempt against a resolved key, or against the Bedrock AWS
1048
+ // credential chain when its optional resolver has no stored bearer key.
1049
+ // Retryable auth failures are buffered until replay is safe.
1050
+ const runAttempt = async (apiKey?: string): Promise<AuthRetryFailure | undefined> => {
1051
+ const bufferedEvents: AssistantMessageEvent[] = [];
1052
+ let emittedReplayUnsafeEvent = false;
1053
+ const flushBuffered = (): void => {
1054
+ emitBufferedEvents(outer, bufferedEvents);
1055
+ bufferedEvents.length = 0;
1056
+ };
1057
+
1058
+ try {
1059
+ const inner = streamSimple(model, context, { ...requestOptions, apiKey });
1060
+ for await (const event of inner) {
1061
+ if (!emittedReplayUnsafeEvent && event.type === "start") {
1062
+ bufferedEvents.push(event);
1063
+ continue;
1064
+ }
1065
+ if (
1066
+ !emittedReplayUnsafeEvent &&
1067
+ event.type === "error" &&
1068
+ isRetryableUpstreamError(
1069
+ event.error,
1070
+ extractStatusFromAssistantError(event.error),
1071
+ event.error.errorMessage,
1072
+ )
1073
+ ) {
1074
+ return { error: createAssistantAuthError(event.error), bufferedEvents, terminalEvent: event };
1075
+ }
1076
+ flushBuffered();
1077
+ emittedReplayUnsafeEvent = true;
1078
+ outer.push(event);
1079
+ if (outer.done) return undefined;
1080
+ }
1081
+ flushBuffered();
1082
+ if (!outer.done) outer.end(await inner.result());
1083
+ } catch (error) {
1084
+ if (
1085
+ !emittedReplayUnsafeEvent &&
1086
+ isRetryableUpstreamError(
1087
+ error,
1088
+ AIError.status(error),
1089
+ error instanceof Error ? error.message : undefined,
1090
+ )
1091
+ ) {
1092
+ return { error, bufferedEvents };
1093
+ }
1094
+ flushBuffered();
1095
+ outer.fail(error);
1096
+ }
1097
+ return undefined;
1098
+ };
1099
+ const emitFailure = (failure: AuthRetryFailure): void => {
1100
+ emitBufferedEvents(outer, failure.bufferedEvents);
1101
+ if (failure.terminalEvent) {
1102
+ outer.push(failure.terminalEvent);
1103
+ } else {
1104
+ outer.fail(failure.error);
1105
+ }
1106
+ };
1107
+
1108
+ void (async () => {
1109
+ let lastKey: string | undefined;
1110
+ try {
1111
+ lastKey = (await apiKeyResolver({ lastChance: false, error: undefined, signal })) || undefined;
1112
+ } catch (error) {
1113
+ // A thrown resolver is a broker/OAuth/network failure, not a missing
1114
+ // key — surface the cause instead of masking it as "No API key".
1115
+ outer.fail(
1116
+ new AIError.ConfigurationError(
1117
+ `Failed to resolve API key for provider ${model.provider}: ${error instanceof Error ? error.message : String(error)}`,
1118
+ { cause: error },
1119
+ ),
1120
+ );
1121
+ return;
1122
+ }
1123
+ if (lastKey === undefined) {
1124
+ if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
1125
+ const failure = await runAttempt();
1126
+ if (failure) emitFailure(failure);
1127
+ return;
1128
+ }
1129
+ outer.fail(new AIError.MissingApiKeyError(model.provider));
1130
+ return;
1131
+ }
1132
+ const retryState = createAuthRetryKeyState(lastKey);
1133
+ let failure = await runAttempt(lastKey);
1134
+ if (!failure) return;
1135
+ while (true) {
1136
+ // Caller aborted between attempts: don't mint a fresh token or fire
1137
+ // another doomed request — emit the captured failure instead.
1138
+ if (signal?.aborted) break;
1139
+ const nextKey = await resolveNextAuthRetryKey(retryState, apiKeyResolver, failure.error, signal);
1140
+ if (nextKey === undefined) break;
1141
+ const next = await runAttempt(nextKey);
1142
+ if (!next) return;
1143
+ failure = next;
1144
+ }
1145
+ emitFailure(failure);
1146
+ })();
1147
+ return outer;
1148
+ }
1149
+
1150
+ // Pi-native transport short-circuits the per-provider dispatch entirely:
1151
+ // the gateway resolves provider + credential server-side, so we don't
1152
+ // need an `apiKey` from `getEnvApiKey` here — `options.apiKey` carries
1153
+ // the gateway bearer instead. Comes BEFORE the custom-API check so
1154
+ // extension-registered APIs can't accidentally override a configured
1155
+ // pi-native transport.
1156
+ if (model.transport === "pi-native") {
1157
+ return withGeminiThinkingLoopGuard(model, requestOptions, opts =>
1158
+ withProviderInFlightLimit(model, opts, () => streamPiNative(model, context, opts)),
1159
+ );
1160
+ }
1161
+
1162
+ // Check custom API registry (extension-provided APIs)
1163
+ const customApiProvider = getCustomApi(model.api);
1164
+ if (customApiProvider) {
1165
+ return withGeminiThinkingLoopGuard(model, requestOptions, opts =>
1166
+ withProviderInFlightLimit(model, opts, () => customApiProvider.streamSimple(model, context, opts)),
1167
+ );
1168
+ }
1169
+
1170
+ // Vertex AI uses Application Default Credentials, not API keys
1171
+ if (model.api === "google-vertex") {
1172
+ const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
1173
+ return stream(model, context, providerOptions);
1174
+ } else if (model.api === "bedrock-converse-stream") {
1175
+ // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
1176
+ const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
1177
+ return stream(model, context, providerOptions);
1178
+ } else if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
1179
+ const providerOptions = mapOptionsForApi(
1180
+ model,
1181
+ requestOptions,
1182
+ typeof requestOptions.apiKey === "string" ? requestOptions.apiKey : getEnvApiKey(model.provider),
1183
+ );
1184
+ return stream(model, context, providerOptions);
1185
+ }
1186
+
1187
+ // The resolver form is handled by the wrapper above; only a static string
1188
+ // key reaches this point.
1189
+ const apiKey =
1190
+ (typeof requestOptions?.apiKey === "string" ? requestOptions.apiKey : undefined) || getEnvApiKey(model.provider);
1191
+ if (!apiKey) {
1192
+ throw new AIError.MissingApiKeyError(model.provider);
1193
+ }
1194
+
1195
+ // GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
1196
+ if (isGitLabDuoModel(model)) {
1197
+ return withProviderInFlightLimit(model, requestOptions, () =>
1198
+ streamGitLabDuo(model, context, {
1199
+ ...requestOptions,
1200
+ apiKey,
1201
+ }),
1202
+ );
1203
+ }
1204
+
1205
+ // GitLab Duo Workflow - IDE workflow protocol + WebSocket action bridge
1206
+ if (model.api === "gitlab-duo-agent") {
1207
+ // Does not route through withProviderInFlightLimit, so heal explicitly.
1208
+ return healLeakedThinking(
1209
+ model,
1210
+ streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
1211
+ ...requestOptions,
1212
+ apiKey,
1213
+ }),
1214
+ );
1215
+ }
1216
+
1217
+ // Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
1218
+ if (isKimiModel(model)) {
1219
+ // streamKimi handles openai/anthropic format mapping internally, but the
1220
+ // mandatory-reasoning clamp is a request-shaping concern owned here: K3's
1221
+ // `supports_thinking_type: "only"` endpoint rejects disabled/omitted
1222
+ // thinking, so clamp disabled requests to the lowest supported effort
1223
+ // (mirrors the mapOptionsForApi path every other provider takes).
1224
+ const kimiOptions = normalizeMandatoryReasoningOptions(model, requestOptions);
1225
+ return withProviderInFlightLimit(model, kimiOptions, () =>
1226
+ streamKimi(model as Model<"openai-completions">, context, {
1227
+ ...kimiOptions,
1228
+ apiKey,
1229
+ format: kimiOptions?.kimiApiFormat,
1230
+ }),
1231
+ );
1232
+ }
1233
+
1234
+ // Synthetic - route to dedicated handler that wraps OpenAI or Anthropic API
1235
+ if (isSyntheticModel(model)) {
1236
+ // Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
1237
+ return withProviderInFlightLimit(model, requestOptions, () =>
1238
+ streamSynthetic(model as Model<"openai-completions">, context, {
1239
+ ...requestOptions,
1240
+ apiKey,
1241
+ format: requestOptions?.syntheticApiFormat ?? "openai", // Default to OpenAI format
1242
+ }),
1243
+ );
1244
+ }
1245
+ const providerOptions = mapOptionsForApi(model, requestOptions, apiKey);
1246
+ return stream(model, context, providerOptions);
1247
+ }
1248
+
1249
+ export async function completeSimple<TApi extends Api>(
1250
+ model: Model<TApi>,
1251
+ context: Context,
1252
+ options?: SimpleStreamOptions,
1253
+ ): Promise<AssistantMessage> {
1254
+ return resolveWithThinkingLoopCook(
1255
+ options?.signal,
1256
+ () => streamSimple(model, context, options),
1257
+ () => streamSimple(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
1258
+ );
1259
+ }
1260
+
1261
+ const MIN_OUTPUT_TOKENS = 1024;
1262
+ // Fallback total output cap for models whose catalog entry has no maxTokens.
1263
+ const OUTPUT_CAP_WHEN_UNKNOWN = 64_000;
1264
+ function maxTokensWithThinkingBudget(
1265
+ baseMaxTokens: number | undefined,
1266
+ modelMaxTokens: number | null,
1267
+ thinkingBudget: number,
1268
+ ): number {
1269
+ const uncappedMaxTokens = baseMaxTokens === undefined ? OUTPUT_CAP_WHEN_UNKNOWN : baseMaxTokens + thinkingBudget;
1270
+ return Math.min(uncappedMaxTokens, modelMaxTokens ?? Number.POSITIVE_INFINITY);
1271
+ }
1272
+ export const OUTPUT_FALLBACK_BUFFER = 4000;
1273
+ const ANTHROPIC_USE_INTERLEAVED_THINKING = Bun.env.PI_NO_INTERLEAVED_THINKING !== "1";
1274
+
1275
+ export const ANTHROPIC_THINKING: Record<Effort, number> = {
1276
+ minimal: 1024,
1277
+ low: 4096,
1278
+ medium: 8192,
1279
+ high: 16384,
1280
+ xhigh: 32768,
1281
+ max: 32768,
1282
+ };
1283
+
1284
+ const GOOGLE_THINKING: Record<Effort, number> = {
1285
+ minimal: 1024,
1286
+ low: 4096,
1287
+ medium: 8192,
1288
+ high: 16384,
1289
+ xhigh: 24575,
1290
+ max: 32768,
1291
+ };
1292
+
1293
+ const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
1294
+ minimal: 1024,
1295
+ low: 2048,
1296
+ medium: 8192,
1297
+ high: 16384,
1298
+ xhigh: 16384,
1299
+ max: 32768,
1300
+ };
1301
+
1302
+ function resolveBedrockThinkingBudget(
1303
+ model: Model<"bedrock-converse-stream">,
1304
+ options?: SimpleStreamOptions,
1305
+ ): { budget: number; level: Effort } | null {
1306
+ if (!options?.reasoning || !model.reasoning) return null;
1307
+ const level = requireSupportedEffort(model, options.reasoning);
1308
+ const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
1309
+ return { budget, level };
1310
+ }
1311
+
1312
+ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"] {
1313
+ if (!choice) return undefined;
1314
+ if (typeof choice === "string") {
1315
+ if (choice === "required") return "any";
1316
+ if (choice === "auto" || choice === "none" || choice === "any") return choice;
1317
+ return undefined;
1318
+ }
1319
+ if (choice.type === "tool") {
1320
+ return choice.name ? { type: "tool", name: choice.name } : undefined;
1321
+ }
1322
+ if (choice.type === "function") {
1323
+ const name = "function" in choice ? choice.function?.name : choice.name;
1324
+ return name ? { type: "tool", name } : undefined;
1325
+ }
1326
+ return undefined;
1327
+ }
1328
+
1329
+ export function mapGoogleToolChoice(
1330
+ choice?: ToolChoice,
1331
+ ): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
1332
+ if (!choice) return undefined;
1333
+ if (typeof choice === "string") {
1334
+ if (choice === "required") return "any";
1335
+ if (choice === "auto" || choice === "none" || choice === "any") return choice;
1336
+ return undefined;
1337
+ }
1338
+ // Named-tool routing on Google: emit an `ANY`-mode allow-list of one entry,
1339
+ // mirroring the Anthropic mapper that returns `{type: "tool", name}`.
1340
+ if (choice.type === "tool") {
1341
+ return choice.name ? { mode: "ANY", allowedFunctionNames: [choice.name] } : undefined;
1342
+ }
1343
+ if (choice.type === "function") {
1344
+ const name = "function" in choice ? choice.function?.name : choice.name;
1345
+ return name ? { mode: "ANY", allowedFunctionNames: [name] } : undefined;
1346
+ }
1347
+ return undefined;
1348
+ }
1349
+
1350
+ function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["toolChoice"] {
1351
+ if (!choice) return undefined;
1352
+ if (typeof choice === "string") {
1353
+ if (choice === "any") return "required";
1354
+ if (choice === "auto" || choice === "none" || choice === "required") return choice;
1355
+ return undefined;
1356
+ }
1357
+ if (choice.type === "tool") {
1358
+ return choice.name ? { type: "function", function: { name: choice.name } } : undefined;
1359
+ }
1360
+ if (choice.type === "function") {
1361
+ const name = "function" in choice ? choice.function?.name : choice.name;
1362
+ return name ? { type: "function", function: { name } } : undefined;
1363
+ }
1364
+ return undefined;
1365
+ }
1366
+
1367
+ type ReasoningEffortMapCompat = {
1368
+ reasoningEffortMap?: Partial<Record<Effort, string>>;
1369
+ };
1370
+
1371
+ function getCompatReasoningEffortMap<TApi extends Api>(
1372
+ model: Model<TApi>,
1373
+ ): Partial<Record<Effort, string>> | undefined {
1374
+ const compat = model.compat;
1375
+ if (compat === undefined || typeof compat !== "object" || !("reasoningEffortMap" in compat)) {
1376
+ return undefined;
1377
+ }
1378
+ return (compat as ReasoningEffortMapCompat).reasoningEffortMap;
1379
+ }
1380
+
1381
+ function resolveSupportedMappedReasoningEffort<TApi extends Api>(
1382
+ model: Model<TApi>,
1383
+ reasoning: Effort,
1384
+ ): Effort | undefined {
1385
+ const mapped = getCompatReasoningEffortMap(model)?.[reasoning];
1386
+ if (!mapped) return undefined;
1387
+ const mappedEffort = mapped as Effort;
1388
+ return model.thinking?.efforts.includes(mappedEffort) ? mappedEffort : undefined;
1389
+ }
1390
+
1391
+ function resolveOpenAiReasoningEffort<TApi extends Api>(
1392
+ model: Model<TApi>,
1393
+ options?: SimpleStreamOptions,
1394
+ ): Effort | undefined {
1395
+ const reasoning = options?.reasoning;
1396
+ if (!reasoning || !model.reasoning) return undefined;
1397
+ // Models that reason natively but expose no effort dial carry
1398
+ // `thinking: undefined` (baked at build time from
1399
+ // `compat.supportsReasoningEffort: false` on openai-responses*). The
1400
+ // wire-side omitReasoningEffort gate (stream.ts) is the actual strip; returning
1401
+ // undefined here avoids a redundant requireSupportedEffort throw that would
1402
+ // defeat the gate and surface a confusing "Compaction failed: Thinking effort
1403
+ // high is not supported by..." to the user.
1404
+ if (!model.thinking) return undefined;
1405
+ if (model.thinking.efforts.includes(reasoning)) return reasoning;
1406
+ const mappedReasoning = resolveSupportedMappedReasoningEffort(model, reasoning);
1407
+ if (mappedReasoning) return mappedReasoning;
1408
+ if (getCompatReasoningEffortMap(model)?.[reasoning] !== undefined) return reasoning;
1409
+ if (model.thinking.effortMap?.[reasoning] !== undefined) return reasoning;
1410
+ return requireSupportedEffort(model, reasoning);
1411
+ }
1412
+
1413
+ const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
1414
+
1415
+ /**
1416
+ * Mandatory-reasoning endpoints (`thinking.requiresEffort`) reject disabled
1417
+ * or omitted thinking ("Reasoning is mandatory for this endpoint and cannot
1418
+ * be disabled") — clamp to the lowest supported effort instead.
1419
+ * `suppressWhenOff` models handle off provider-side via explicit wire
1420
+ * suppression. Collapsed pairs interplay: pair derivation strips member
1421
+ * flags (off routes to a bare SKU that CAN disable), while identity backfill
1422
+ * re-flags pairs whose logical id is itself mandatory (Gemini 3.x) — there
1423
+ * the clamp wins and the floored effort routes to the thinking SKU.
1424
+ */
1425
+ function normalizeMandatoryReasoningOptions<TApi extends Api>(
1426
+ model: Model<TApi>,
1427
+ options?: SimpleStreamOptions,
1428
+ ): SimpleStreamOptions | undefined {
1429
+ if (
1430
+ !model.reasoning ||
1431
+ !model.thinking?.requiresEffort ||
1432
+ model.thinking.suppressWhenOff ||
1433
+ (options?.reasoning !== undefined && !options.disableReasoning)
1434
+ ) {
1435
+ return options;
1436
+ }
1437
+ const floor = minimumSupportedEffort(model);
1438
+ if (floor === undefined) return options;
1439
+ return { ...options, reasoning: floor, disableReasoning: undefined };
1440
+ }
1441
+
1442
+ function supportsExplicitOpenAIResponsesPromptCache(compat: unknown): boolean {
1443
+ return (
1444
+ typeof compat === "object" &&
1445
+ compat !== null &&
1446
+ "supportsPromptCacheBreakpoints" in compat &&
1447
+ compat.supportsPromptCacheBreakpoints === true
1448
+ );
1449
+ }
1450
+
1451
+ function isOpenAIResponsesPromptCacheSurface<TApi extends Api>(model: Model<TApi>): boolean {
1452
+ return (
1453
+ model.api === "openai-responses" ||
1454
+ model.api === "azure-openai-responses" ||
1455
+ (model.api === "openrouter" && $env.PI_OPENROUTER_RESPONSES !== "0")
1456
+ );
1457
+ }
1458
+
1459
+ function assertExplicitOpenAIResponsesPromptCacheSupport<TApi extends Api>(
1460
+ model: Model<TApi>,
1461
+ options?: StreamOptions,
1462
+ ): void {
1463
+ if (
1464
+ model.transport === "pi-native" ||
1465
+ resolveCacheRetention(options?.cacheRetention) === "none" ||
1466
+ options?.promptCache?.mode !== "explicit" ||
1467
+ !isOpenAIResponsesPromptCacheSurface(model) ||
1468
+ supportsExplicitOpenAIResponsesPromptCache(model.compat)
1469
+ ) {
1470
+ return;
1471
+ }
1472
+ throw new AIError.ConfigurationError(
1473
+ `OpenAI explicit prompt caching is unsupported for ${model.provider}/${model.id}; enable compat.supportsPromptCacheBreakpoints only for a compatible endpoint.`,
1474
+ );
1475
+ }
1476
+
1477
+ function mapOptionsForApi<TApi extends Api>(
1478
+ model: Model<TApi>,
1479
+ rawOptions?: SimpleStreamOptions,
1480
+ apiKey?: string,
1481
+ ): OptionsForApi<TApi> {
1482
+ const options = normalizeMandatoryReasoningOptions(model, rawOptions);
1483
+ const simpleProviderOptions = getProviderDefinition(model.provider)?.mapSimpleOptions?.(options ?? {});
1484
+ const base = {
1485
+ temperature: options?.temperature,
1486
+ topP: options?.topP,
1487
+ topK: options?.topK,
1488
+ minP: options?.minP,
1489
+ presencePenalty: options?.presencePenalty,
1490
+ repetitionPenalty: options?.repetitionPenalty,
1491
+ maxTokens: options?.maxTokens ?? model.maxTokens ?? undefined,
1492
+ signal: options?.signal,
1493
+ apiKey: apiKey ?? (typeof options?.apiKey === "string" ? options.apiKey : undefined),
1494
+ cacheRetention: options?.cacheRetention,
1495
+ headers: options?.headers,
1496
+ initiatorOverride: options?.initiatorOverride,
1497
+ maxRetryDelayMs: options?.maxRetryDelayMs,
1498
+ metadata: options?.metadata,
1499
+ taskBudget: options?.taskBudget,
1500
+ sessionId: options?.sessionId,
1501
+ promptCacheKey: options?.promptCacheKey,
1502
+ streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
1503
+ streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
1504
+ codexSseMaxAttempts: options?.codexSseMaxAttempts,
1505
+ providerSessionState: options?.providerSessionState,
1506
+ maxInFlightRequests: options?.maxInFlightRequests,
1507
+ onPayload: options?.onPayload,
1508
+ onResponse: options?.onResponse,
1509
+ onSseEvent: options?.onSseEvent,
1510
+ execHandlers: options?.execHandlers,
1511
+ fetch: options?.fetch,
1512
+ fallbacks: options?.fallbacks,
1513
+ ...simpleProviderOptions,
1514
+ };
1515
+
1516
+ switch (model.api) {
1517
+ case "anthropic-messages": {
1518
+ // Explicitly disable thinking when reasoning is not specified, the caller
1519
+ // disabled it, or the model doesn't support it. `disableReasoning` is a
1520
+ // SimpleStreamOptions flag that never reaches AnthropicOptions on its own,
1521
+ // so it must be folded into `thinkingEnabled` here (mandatory-reasoning
1522
+ // models already clamp it away in normalizeMandatoryReasoningOptions).
1523
+ const reasoning = options?.reasoning;
1524
+ if (!reasoning || !model.reasoning || options?.disableReasoning) {
1525
+ return castApi<"anthropic-messages">({
1526
+ ...base,
1527
+ requestModelId: resolveWireModelId(model, undefined),
1528
+ thinkingEnabled: false,
1529
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1530
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1531
+ serviceTier: options?.serviceTier,
1532
+ });
1533
+ }
1534
+
1535
+ let thinkingBudget = options.thinkingBudgets?.[reasoning] ?? ANTHROPIC_THINKING[reasoning];
1536
+ if (thinkingBudget <= 0) {
1537
+ return castApi<"anthropic-messages">({
1538
+ ...base,
1539
+ requestModelId: resolveWireModelId(model, undefined),
1540
+ thinkingEnabled: false,
1541
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1542
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1543
+ serviceTier: options?.serviceTier,
1544
+ });
1545
+ }
1546
+
1547
+ const thinkingMode = model.thinking?.mode;
1548
+ const effort =
1549
+ thinkingMode === "anthropic-adaptive" || thinkingMode === "anthropic-budget-effort"
1550
+ ? mapEffortToAnthropicAdaptiveEffort(model, reasoning)
1551
+ : undefined;
1552
+
1553
+ // For Opus 4.6+ and Sonnet 4.6+: use adaptive thinking with effort level
1554
+ // For older models: use budget-based thinking
1555
+ if (thinkingMode === "anthropic-adaptive") {
1556
+ return castApi<"anthropic-messages">({
1557
+ ...base,
1558
+ requestModelId: resolveWireModelId(model, reasoning),
1559
+ thinkingEnabled: true,
1560
+ effort,
1561
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1562
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1563
+ serviceTier: options?.serviceTier,
1564
+ });
1565
+ }
1566
+
1567
+ if (ANTHROPIC_USE_INTERLEAVED_THINKING) {
1568
+ return castApi<"anthropic-messages">({
1569
+ ...base,
1570
+ requestModelId: resolveWireModelId(model, reasoning),
1571
+ thinkingEnabled: true,
1572
+ thinkingBudgetTokens: thinkingBudget,
1573
+ effort,
1574
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1575
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1576
+ serviceTier: options?.serviceTier,
1577
+ });
1578
+ }
1579
+
1580
+ // Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
1581
+ const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
1582
+
1583
+ // If not enough room for thinking + output, reduce thinking budget
1584
+ if (maxTokens <= thinkingBudget) {
1585
+ thinkingBudget = maxTokens - MIN_OUTPUT_TOKENS;
1586
+ }
1587
+
1588
+ // If thinking budget is too low, disable thinking
1589
+ if (thinkingBudget <= 0) {
1590
+ return castApi<"anthropic-messages">({
1591
+ ...base,
1592
+ requestModelId: resolveWireModelId(model, undefined),
1593
+ thinkingEnabled: false,
1594
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1595
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1596
+ serviceTier: options?.serviceTier,
1597
+ });
1598
+ } else {
1599
+ return castApi<"anthropic-messages">({
1600
+ ...base,
1601
+ maxTokens,
1602
+ requestModelId: resolveWireModelId(model, reasoning),
1603
+ thinkingEnabled: true,
1604
+ thinkingBudgetTokens: thinkingBudget,
1605
+ effort,
1606
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1607
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1608
+ serviceTier: options?.serviceTier,
1609
+ });
1610
+ }
1611
+ }
1612
+
1613
+ case "bedrock-converse-stream": {
1614
+ const bedrockBase: BedrockOptions = {
1615
+ ...base,
1616
+ reasoning: options?.reasoning,
1617
+ thinkingBudgets: options?.thinkingBudgets,
1618
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
1619
+ thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
1620
+ };
1621
+ // Adaptive mode sends effort directly, no budget_tokens — skip budget inflation.
1622
+ if (model.thinking?.mode === "anthropic-adaptive") {
1623
+ return castApi<"bedrock-converse-stream">(bedrockBase);
1624
+ }
1625
+ const budgetInfo = resolveBedrockThinkingBudget(model as Model<"bedrock-converse-stream">, options);
1626
+ if (!budgetInfo) return bedrockBase as OptionsForApi<TApi>;
1627
+ let maxTokens = bedrockBase.maxTokens ?? model.maxTokens ?? OUTPUT_CAP_WHEN_UNKNOWN;
1628
+ let thinkingBudgets = bedrockBase.thinkingBudgets;
1629
+ if (maxTokens <= budgetInfo.budget) {
1630
+ const desiredMaxTokens = Math.min(
1631
+ model.maxTokens ?? Number.POSITIVE_INFINITY,
1632
+ budgetInfo.budget + MIN_OUTPUT_TOKENS,
1633
+ );
1634
+ if (desiredMaxTokens > maxTokens) {
1635
+ maxTokens = desiredMaxTokens;
1636
+ }
1637
+ }
1638
+ if (maxTokens <= budgetInfo.budget) {
1639
+ const adjustedBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
1640
+ thinkingBudgets = { ...(thinkingBudgets ?? {}), [budgetInfo.level]: adjustedBudget };
1641
+ }
1642
+ return castApi<"bedrock-converse-stream">({ ...bedrockBase, maxTokens, thinkingBudgets });
1643
+ }
1644
+
1645
+ case "openrouter": {
1646
+ const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
1647
+ if (useResponses) {
1648
+ return castApi<"openai-responses">({
1649
+ ...base,
1650
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1651
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1652
+ serviceTier: options?.serviceTier,
1653
+ reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1654
+ openrouterVariant: options?.openrouterVariant,
1655
+ maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1656
+ disableReasoning: options?.disableReasoning,
1657
+ textVerbosity: options?.textVerbosity,
1658
+ promptCache: options?.promptCache,
1659
+ statefulResponses: options?.statefulResponses,
1660
+ });
1661
+ }
1662
+ return castApi<"openai-completions">({
1663
+ ...base,
1664
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1665
+ disableReasoning: options?.disableReasoning,
1666
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1667
+ serviceTier: options?.serviceTier,
1668
+ openrouterVariant: options?.openrouterVariant,
1669
+ maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1670
+ promptCache: options?.promptCache,
1671
+ });
1672
+ }
1673
+
1674
+ case "openai-completions":
1675
+ return castApi<"openai-completions">({
1676
+ ...base,
1677
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1678
+ disableReasoning: options?.disableReasoning,
1679
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1680
+ serviceTier: options?.serviceTier,
1681
+ openrouterVariant: options?.openrouterVariant,
1682
+ maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1683
+ promptCache: options?.promptCache,
1684
+ });
1685
+
1686
+ case "openai-responses":
1687
+ return castApi<"openai-responses">({
1688
+ ...base,
1689
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1690
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1691
+ serviceTier: options?.serviceTier,
1692
+ reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1693
+ openrouterVariant: options?.openrouterVariant,
1694
+ maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1695
+ disableReasoning: options?.disableReasoning,
1696
+ textVerbosity: options?.textVerbosity,
1697
+ promptCache: options?.promptCache,
1698
+ statefulResponses: options?.statefulResponses,
1699
+ });
1700
+
1701
+ case "azure-openai-responses":
1702
+ return castApi<"azure-openai-responses">({
1703
+ ...base,
1704
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1705
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1706
+ serviceTier: options?.serviceTier,
1707
+ reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1708
+ promptCache: options?.promptCache,
1709
+ statefulResponses: options?.statefulResponses,
1710
+ });
1711
+
1712
+ case "openai-codex-responses":
1713
+ return castApi<"openai-codex-responses">({
1714
+ ...base,
1715
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1716
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1717
+ serviceTier: options?.serviceTier,
1718
+ preferWebsockets: options?.preferWebsockets,
1719
+ codexCompaction: options?.codexCompaction,
1720
+ reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1721
+ textVerbosity: options?.textVerbosity,
1722
+ });
1723
+
1724
+ case "google-generative-ai": {
1725
+ // Explicitly disable thinking when reasoning is not specified or model doesn't support it
1726
+ // This is needed because Gemini has "dynamic thinking" enabled by default
1727
+ const reasoning = options?.reasoning;
1728
+ if (!reasoning || !model.reasoning) {
1729
+ return castApi<"google-generative-ai">({
1730
+ ...base,
1731
+ serviceTier: options?.serviceTier,
1732
+ thinking: { enabled: false },
1733
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1734
+ cachedContent: options?.cachedContent,
1735
+ });
1736
+ }
1737
+
1738
+ const googleModel = model as Model<"google-generative-ai">;
1739
+ const effort = requireSupportedEffort(googleModel, reasoning);
1740
+
1741
+ // Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
1742
+ // https://ai.google.dev/gemini-api/docs/thinking#set-budget
1743
+ if (googleModel.thinking?.mode === "google-level") {
1744
+ return castApi<"google-generative-ai">({
1745
+ ...base,
1746
+ serviceTier: options?.serviceTier,
1747
+ thinking: {
1748
+ enabled: true,
1749
+ level: mapEffortToGoogleThinkingLevel(effort),
1750
+ },
1751
+ hideThinkingSummary: options?.hideThinkingSummary,
1752
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1753
+ cachedContent: options?.cachedContent,
1754
+ });
1755
+ }
1756
+
1757
+ return castApi<"google-generative-ai">({
1758
+ ...base,
1759
+ thinking: {
1760
+ enabled: true,
1761
+ budgetTokens: getGoogleBudget(googleModel, effort, options?.thinkingBudgets),
1762
+ },
1763
+ hideThinkingSummary: options?.hideThinkingSummary,
1764
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1765
+ cachedContent: options?.cachedContent,
1766
+ });
1767
+ }
1768
+
1769
+ case "google-gemini-cli": {
1770
+ const reasoning = options?.reasoning;
1771
+ const toolChoice = mapGoogleToolChoice(options?.toolChoice);
1772
+ if (reasoning && model.reasoning) {
1773
+ const effort = requireSupportedEffort(model, reasoning);
1774
+
1775
+ // Gemini 3+ models use thinkingLevel instead of thinkingBudget
1776
+ if (model.thinking?.mode === "google-level") {
1777
+ return castApi<"google-gemini-cli">({
1778
+ ...base,
1779
+ requestModelId: resolveWireModelId(model, effort),
1780
+ thinking: {
1781
+ enabled: true,
1782
+ level: mapEffortToGoogleThinkingLevel(effort),
1783
+ },
1784
+ hideThinkingSummary: options?.hideThinkingSummary,
1785
+ toolChoice,
1786
+ antigravityEndpointMode: options?.antigravityEndpointMode,
1787
+ });
1788
+ }
1789
+
1790
+ let thinkingBudget =
1791
+ options.thinkingBudgets?.[effort] ?? model.thinking?.effortBudgets?.[effort] ?? GOOGLE_THINKING[effort];
1792
+
1793
+ // Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
1794
+ const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
1795
+
1796
+ // If not enough room for thinking + output, reduce thinking budget
1797
+ if (maxTokens <= thinkingBudget) {
1798
+ thinkingBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
1799
+ }
1800
+
1801
+ if (thinkingBudget > 0) {
1802
+ return castApi<"google-gemini-cli">({
1803
+ ...base,
1804
+ maxTokens,
1805
+ requestModelId: resolveWireModelId(model, effort),
1806
+ thinking: { enabled: true, budgetTokens: thinkingBudget },
1807
+ hideThinkingSummary: options?.hideThinkingSummary,
1808
+ toolChoice,
1809
+ antigravityEndpointMode: options?.antigravityEndpointMode,
1810
+ });
1811
+ }
1812
+ // Budget clamped to zero — fall through to the thinking-off path.
1813
+ }
1814
+
1815
+ const thinking: GoogleGeminiCliOptions["thinking"] = { enabled: false };
1816
+ if (model.reasoning && model.thinking?.suppressWhenOff) {
1817
+ // CCA re-applies the per-id baked server default when the config
1818
+ // is omitted; suppression must be explicit on the wire.
1819
+ thinking.suppress = model.thinking.mode === "google-level" ? { level: "MINIMAL" } : { budget: 0 };
1820
+ }
1821
+ return castApi<"google-gemini-cli">({
1822
+ ...base,
1823
+ requestModelId: resolveWireModelId(model, undefined),
1824
+ thinking,
1825
+ toolChoice,
1826
+ antigravityEndpointMode: options?.antigravityEndpointMode,
1827
+ });
1828
+ }
1829
+
1830
+ case "google-vertex": {
1831
+ // Explicitly disable thinking when reasoning is not specified or model doesn't support it
1832
+ const reasoning = options?.reasoning;
1833
+ if (!reasoning || !model.reasoning) {
1834
+ return castApi<"google-vertex">({
1835
+ ...base,
1836
+ serviceTier: options?.serviceTier,
1837
+ thinking: { enabled: false },
1838
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1839
+ cachedContent: options?.cachedContent,
1840
+ });
1841
+ }
1842
+
1843
+ const vertexModel = model as Model<"google-vertex">;
1844
+ const effort = requireSupportedEffort(vertexModel, reasoning);
1845
+ const geminiModel = vertexModel as unknown as Model<"google-generative-ai">;
1846
+
1847
+ if (geminiModel.thinking?.mode === "google-level") {
1848
+ return castApi<"google-vertex">({
1849
+ ...base,
1850
+ serviceTier: options?.serviceTier,
1851
+ thinking: {
1852
+ enabled: true,
1853
+ level: mapEffortToGoogleThinkingLevel(effort),
1854
+ },
1855
+ hideThinkingSummary: options?.hideThinkingSummary,
1856
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1857
+ cachedContent: options?.cachedContent,
1858
+ });
1859
+ }
1860
+
1861
+ return castApi<"google-vertex">({
1862
+ ...base,
1863
+ serviceTier: options?.serviceTier,
1864
+ thinking: {
1865
+ enabled: true,
1866
+ budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
1867
+ },
1868
+ hideThinkingSummary: options?.hideThinkingSummary,
1869
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
1870
+ cachedContent: options?.cachedContent,
1871
+ });
1872
+ }
1873
+
1874
+ case "ollama-chat":
1875
+ return castApi<"ollama-chat">({
1876
+ ...base,
1877
+ reasoning: resolveOpenAiReasoningEffort(model, options),
1878
+ disableReasoning: options?.disableReasoning,
1879
+ toolChoice: options?.toolChoice,
1880
+ });
1881
+
1882
+ case "cursor-agent": {
1883
+ const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
1884
+ const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
1885
+ return castApi<"cursor-agent">({
1886
+ ...base,
1887
+ execHandlers,
1888
+ onToolResult,
1889
+ });
1890
+ }
1891
+
1892
+ case "gitlab-duo-agent":
1893
+ return castApi<"gitlab-duo-agent">({
1894
+ ...base,
1895
+ cwd: options?.cwd,
1896
+ toolChoice: options?.toolChoice,
1897
+ });
1898
+ case "devin-agent": {
1899
+ const devinModel = model as Model<"devin-agent">;
1900
+ const effort =
1901
+ options?.reasoning && !options.disableReasoning
1902
+ ? requireSupportedEffort(devinModel, options.reasoning)
1903
+ : undefined;
1904
+ return castApi<"devin-agent">({
1905
+ ...base,
1906
+ chatModelUid: resolveWireModelId(devinModel, effort),
1907
+ });
1908
+ }
1909
+ default:
1910
+ throw new AIError.ConfigurationError(`Unhandled API in mapOptionsForApi: ${model.api}`);
1911
+ }
1912
+ }
1913
+
1914
+ function getGoogleBudget(
1915
+ model: Model<"google-generative-ai">,
1916
+ effort: Effort,
1917
+ customBudgets?: ThinkingBudgets,
1918
+ ): number {
1919
+ requireSupportedEffort(model, effort);
1920
+
1921
+ // Custom budgets take precedence if provided for this level
1922
+ if (customBudgets?.[effort] !== undefined) {
1923
+ return customBudgets[effort]!;
1924
+ }
1925
+
1926
+ // See https://ai.google.dev/gemini-api/docs/thinking#set-budget
1927
+ if (model.id.includes("2.5-")) {
1928
+ switch (effort) {
1929
+ case "minimal":
1930
+ return 128;
1931
+ case "low":
1932
+ return 2048;
1933
+ case "medium":
1934
+ return 8192;
1935
+ case "high":
1936
+ case "xhigh":
1937
+ case "max":
1938
+ return model.id.includes("2.5-flash") ? 24576 : 32768;
1939
+ }
1940
+ }
1941
+
1942
+ // Unknown model - use dynamic
1943
+ return -1;
1944
+ }