@vib-rato/ai 0.16.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 (525) hide show
  1. package/CHANGELOG.md +3354 -0
  2. package/README.md +1194 -0
  3. package/dist/types/adapter-internals/provider-safety-stop.d.ts +45 -0
  4. package/dist/types/api-registry.d.ts +30 -0
  5. package/dist/types/auth-broker/client.d.ts +80 -0
  6. package/dist/types/auth-broker/index.d.ts +5 -0
  7. package/dist/types/auth-broker/redact.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 +153 -0
  10. package/dist/types/auth-broker/server.d.ts +32 -0
  11. package/dist/types/auth-broker/types.d.ts +132 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +555 -0
  13. package/dist/types/auth-gateway/http.d.ts +40 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +70 -0
  16. package/dist/types/auth-gateway/types.d.ts +129 -0
  17. package/dist/types/auth-storage.d.ts +1074 -0
  18. package/dist/types/cli.d.ts +2 -0
  19. package/dist/types/codex-tools.d.ts +4 -0
  20. package/dist/types/context-cap-policy.d.ts +68 -0
  21. package/dist/types/core.d.ts +35 -0
  22. package/dist/types/index.d.ts +55 -0
  23. package/dist/types/model-cache.d.ts +24 -0
  24. package/dist/types/model-manager.d.ts +77 -0
  25. package/dist/types/model-pricing.d.ts +3 -0
  26. package/dist/types/model-retirements.d.ts +6 -0
  27. package/dist/types/model-thinking.d.ts +100 -0
  28. package/dist/types/models.d.ts +21 -0
  29. package/dist/types/openai-completions-compat.d.ts +34 -0
  30. package/dist/types/provider-details.d.ts +24 -0
  31. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  32. package/dist/types/provider-models/descriptors.d.ts +48 -0
  33. package/dist/types/provider-models/google.d.ts +20 -0
  34. package/dist/types/provider-models/index.d.ts +5 -0
  35. package/dist/types/provider-models/ollama.d.ts +7 -0
  36. package/dist/types/provider-models/openai-compat.d.ts +293 -0
  37. package/dist/types/provider-models/special.d.ts +29 -0
  38. package/dist/types/providers/amazon-bedrock.d.ts +60 -0
  39. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  40. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  41. package/dist/types/providers/anthropic.d.ts +280 -0
  42. package/dist/types/providers/aws-credential-config.d.ts +19 -0
  43. package/dist/types/providers/aws-credentials.d.ts +43 -0
  44. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  45. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  46. package/dist/types/providers/azure-openai-responses.d.ts +22 -0
  47. package/dist/types/providers/composer-discipline.d.ts +32 -0
  48. package/dist/types/providers/cursor/client-version.d.ts +10 -0
  49. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  50. package/dist/types/providers/cursor/gen/agent_pb.d.ts +16769 -0
  51. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  52. package/dist/types/providers/cursor.d.ts +72 -0
  53. package/dist/types/providers/dashscope-token-plan-headers.d.ts +57 -0
  54. package/dist/types/providers/error-message.d.ts +27 -0
  55. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  56. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  57. package/dist/types/providers/google-auth.d.ts +26 -0
  58. package/dist/types/providers/google-gemini-cli.d.ts +75 -0
  59. package/dist/types/providers/google-gemini-headers.d.ts +43 -0
  60. package/dist/types/providers/google-shared.d.ts +183 -0
  61. package/dist/types/providers/google-types.d.ts +138 -0
  62. package/dist/types/providers/google-vertex.d.ts +11 -0
  63. package/dist/types/providers/google.d.ts +4 -0
  64. package/dist/types/providers/grammar.d.ts +1 -0
  65. package/dist/types/providers/kimi.d.ts +27 -0
  66. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  67. package/dist/types/providers/kiro-codewhisperer.d.ts +11 -0
  68. package/dist/types/providers/mock.d.ts +189 -0
  69. package/dist/types/providers/ollama.d.ts +41 -0
  70. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  71. package/dist/types/providers/openai-bounded-rate-limits.d.ts +3 -0
  72. package/dist/types/providers/openai-chat-server-schema.d.ts +1733 -0
  73. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  74. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  75. package/dist/types/providers/openai-codex/request-transformer.d.ts +50 -0
  76. package/dist/types/providers/openai-codex/response-handler.d.ts +18 -0
  77. package/dist/types/providers/openai-codex-responses.d.ts +71 -0
  78. package/dist/types/providers/openai-completions-compat.d.ts +6 -0
  79. package/dist/types/providers/openai-completions.d.ts +35 -0
  80. package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
  81. package/dist/types/providers/openai-request-transform.d.ts +4 -0
  82. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  83. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  84. package/dist/types/providers/openai-responses-shared.d.ts +106 -0
  85. package/dist/types/providers/openai-responses.d.ts +37 -0
  86. package/dist/types/providers/pi-native-client.d.ts +29 -0
  87. package/dist/types/providers/pi-native-server.d.ts +60 -0
  88. package/dist/types/providers/register-builtins.d.ts +59 -0
  89. package/dist/types/providers/synthetic.d.ts +26 -0
  90. package/dist/types/providers/transform-messages.d.ts +33 -0
  91. package/dist/types/providers/vision-guard.d.ts +8 -0
  92. package/dist/types/rate-limit-utils.d.ts +19 -0
  93. package/dist/types/stream.d.ts +45 -0
  94. package/dist/types/types.d.ts +1073 -0
  95. package/dist/types/usage/claude.d.ts +3 -0
  96. package/dist/types/usage/gemini.d.ts +2 -0
  97. package/dist/types/usage/github-copilot.d.ts +7 -0
  98. package/dist/types/usage/google-antigravity.d.ts +2 -0
  99. package/dist/types/usage/grok-cli.d.ts +17 -0
  100. package/dist/types/usage/kimi.d.ts +4 -0
  101. package/dist/types/usage/minimax-code.d.ts +2 -0
  102. package/dist/types/usage/openai-codex.d.ts +3 -0
  103. package/dist/types/usage/shared.d.ts +1 -0
  104. package/dist/types/usage/zai.d.ts +2 -0
  105. package/dist/types/usage.d.ts +264 -0
  106. package/dist/types/utils/abort.d.ts +19 -0
  107. package/dist/types/utils/anthropic-auth.d.ts +39 -0
  108. package/dist/types/utils/block-symbols.d.ts +6 -0
  109. package/dist/types/utils/discovery/antigravity.d.ts +67 -0
  110. package/dist/types/utils/discovery/codex.d.ts +38 -0
  111. package/dist/types/utils/discovery/cursor.d.ts +49 -0
  112. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  113. package/dist/types/utils/discovery/index.d.ts +4 -0
  114. package/dist/types/utils/discovery/openai-compatible.d.ts +81 -0
  115. package/dist/types/utils/event-stream.d.ts +41 -0
  116. package/dist/types/utils/fallback-transport.d.ts +110 -0
  117. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  118. package/dist/types/utils/foundry.d.ts +11 -0
  119. package/dist/types/utils/h2-fetch.d.ts +22 -0
  120. package/dist/types/utils/http-inspector.d.ts +59 -0
  121. package/dist/types/utils/idle-iterator.d.ts +122 -0
  122. package/dist/types/utils/json-parse.d.ts +98 -0
  123. package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
  124. package/dist/types/utils/oauth/anthropic.d.ts +41 -0
  125. package/dist/types/utils/oauth/api-key-login.d.ts +38 -0
  126. package/dist/types/utils/oauth/api-key-validation.d.ts +39 -0
  127. package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
  128. package/dist/types/utils/oauth/callback-server.d.ts +80 -0
  129. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  130. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  131. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  132. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  133. package/dist/types/utils/oauth/deepinfra.d.ts +1 -0
  134. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  135. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  136. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  137. package/dist/types/utils/oauth/fugu.d.ts +1 -0
  138. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  139. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  140. package/dist/types/utils/oauth/glm-zcode.d.ts +71 -0
  141. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  142. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  143. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  144. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  145. package/dist/types/utils/oauth/index.d.ts +39 -0
  146. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  147. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  148. package/dist/types/utils/oauth/kimi.d.ts +17 -0
  149. package/dist/types/utils/oauth/kiro.d.ts +71 -0
  150. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  151. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  152. package/dist/types/utils/oauth/mara.d.ts +1 -0
  153. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  154. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  155. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  156. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  157. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  158. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  159. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  160. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  161. package/dist/types/utils/oauth/opengateway.d.ts +1 -0
  162. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  163. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  164. package/dist/types/utils/oauth/perplexity.d.ts +4 -0
  165. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  166. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  167. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  168. package/dist/types/utils/oauth/sglang.d.ts +16 -0
  169. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  170. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  171. package/dist/types/utils/oauth/together.d.ts +1 -0
  172. package/dist/types/utils/oauth/types.d.ts +56 -0
  173. package/dist/types/utils/oauth/venice.d.ts +18 -0
  174. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  175. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  176. package/dist/types/utils/oauth/xai.d.ts +30 -0
  177. package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
  178. package/dist/types/utils/oauth/zai.d.ts +18 -0
  179. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  180. package/dist/types/utils/overflow.d.ts +14 -0
  181. package/dist/types/utils/parse-bind.d.ts +26 -0
  182. package/dist/types/utils/provider-response.d.ts +6 -0
  183. package/dist/types/utils/provider-safety-stop.d.ts +8 -0
  184. package/dist/types/utils/proxy.d.ts +7 -0
  185. package/dist/types/utils/retry-after.d.ts +3 -0
  186. package/dist/types/utils/retry-budget.d.ts +1 -0
  187. package/dist/types/utils/retry.d.ts +29 -0
  188. package/dist/types/utils/schema/adapt.d.ts +24 -0
  189. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  190. package/dist/types/utils/schema/dereference.d.ts +11 -0
  191. package/dist/types/utils/schema/draft.d.ts +10 -0
  192. package/dist/types/utils/schema/equality.d.ts +4 -0
  193. package/dist/types/utils/schema/fields.d.ts +49 -0
  194. package/dist/types/utils/schema/index.d.ts +14 -0
  195. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  196. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  197. package/dist/types/utils/schema/normalize.d.ts +93 -0
  198. package/dist/types/utils/schema/root-combinator.d.ts +12 -0
  199. package/dist/types/utils/schema/spill.d.ts +8 -0
  200. package/dist/types/utils/schema/stamps.d.ts +25 -0
  201. package/dist/types/utils/schema/types.d.ts +4 -0
  202. package/dist/types/utils/schema/wire.d.ts +54 -0
  203. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  204. package/dist/types/utils/sse-debug.d.ts +10 -0
  205. package/dist/types/utils/tool-call-healing.d.ts +80 -0
  206. package/dist/types/utils/tool-choice-capability.d.ts +57 -0
  207. package/dist/types/utils/tool-choice.d.ts +50 -0
  208. package/dist/types/utils/validation.d.ts +17 -0
  209. package/dist/types/utils.d.ts +117 -0
  210. package/package.json +152 -0
  211. package/src/adapter-internals/provider-safety-stop.d.ts +45 -0
  212. package/src/adapter-internals/provider-safety-stop.ts +156 -0
  213. package/src/api-registry.d.ts +30 -0
  214. package/src/api-registry.ts +96 -0
  215. package/src/auth-broker/client.ts +444 -0
  216. package/src/auth-broker/index.ts +5 -0
  217. package/src/auth-broker/redact.ts +39 -0
  218. package/src/auth-broker/refresher.ts +130 -0
  219. package/src/auth-broker/remote-store.ts +1576 -0
  220. package/src/auth-broker/server.ts +764 -0
  221. package/src/auth-broker/types.ts +164 -0
  222. package/src/auth-broker/wire-schemas.ts +261 -0
  223. package/src/auth-gateway/http.ts +198 -0
  224. package/src/auth-gateway/index.ts +3 -0
  225. package/src/auth-gateway/server.ts +1315 -0
  226. package/src/auth-gateway/types.ts +160 -0
  227. package/src/auth-storage.ts +7312 -0
  228. package/src/cli.ts +269 -0
  229. package/src/codex-tools.d.ts +4 -0
  230. package/src/codex-tools.ts +24 -0
  231. package/src/context-cap-policy.d.ts +68 -0
  232. package/src/context-cap-policy.ts +123 -0
  233. package/src/core.ts +44 -0
  234. package/src/index.ts +61 -0
  235. package/src/model-cache.ts +236 -0
  236. package/src/model-manager.ts +744 -0
  237. package/src/model-pricing.d.ts +3 -0
  238. package/src/model-pricing.ts +68 -0
  239. package/src/model-retirements.d.ts +6 -0
  240. package/src/model-retirements.ts +19 -0
  241. package/src/model-thinking.d.ts +100 -0
  242. package/src/model-thinking.ts +1054 -0
  243. package/src/models.d.ts +21 -0
  244. package/src/models.json +94672 -0
  245. package/src/models.json.d.ts +9 -0
  246. package/src/models.ts +126 -0
  247. package/src/openai-completions-compat.d.ts +34 -0
  248. package/src/openai-completions-compat.ts +383 -0
  249. package/src/prompts/composer-bash-policy-recovery.md +1 -0
  250. package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
  251. package/src/prompts/cursor-composer-edit-discipline.md +7 -0
  252. package/src/prompts/turn-aborted-guidance.md +4 -0
  253. package/src/provider-details.ts +90 -0
  254. package/src/provider-models/bundled-references.ts +38 -0
  255. package/src/provider-models/descriptors.ts +392 -0
  256. package/src/provider-models/google.ts +92 -0
  257. package/src/provider-models/index.ts +5 -0
  258. package/src/provider-models/ollama.ts +159 -0
  259. package/src/provider-models/openai-compat.ts +2920 -0
  260. package/src/provider-models/special.ts +185 -0
  261. package/src/providers/amazon-bedrock.d.ts +60 -0
  262. package/src/providers/amazon-bedrock.ts +939 -0
  263. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  264. package/src/providers/anthropic-messages-server.ts +839 -0
  265. package/src/providers/anthropic.d.ts +280 -0
  266. package/src/providers/anthropic.ts +4421 -0
  267. package/src/providers/aws-credential-config.d.ts +19 -0
  268. package/src/providers/aws-credential-config.ts +179 -0
  269. package/src/providers/aws-credentials.d.ts +43 -0
  270. package/src/providers/aws-credentials.ts +457 -0
  271. package/src/providers/aws-eventstream.d.ts +38 -0
  272. package/src/providers/aws-eventstream.ts +185 -0
  273. package/src/providers/aws-sigv4.d.ts +55 -0
  274. package/src/providers/aws-sigv4.ts +218 -0
  275. package/src/providers/azure-openai-responses.d.ts +22 -0
  276. package/src/providers/azure-openai-responses.ts +447 -0
  277. package/src/providers/composer-discipline.d.ts +32 -0
  278. package/src/providers/composer-discipline.ts +95 -0
  279. package/src/providers/cursor/client-version.d.ts +10 -0
  280. package/src/providers/cursor/client-version.ts +10 -0
  281. package/src/providers/cursor/exec-modern.d.ts +98 -0
  282. package/src/providers/cursor/exec-modern.ts +497 -0
  283. package/src/providers/cursor/gen/agent_pb.d.ts +16769 -0
  284. package/src/providers/cursor/gen/agent_pb.ts +19780 -0
  285. package/src/providers/cursor/proto/agent.proto +4533 -0
  286. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  287. package/src/providers/cursor/proto/buf.yaml +17 -0
  288. package/src/providers/cursor-pi-args.d.ts +119 -0
  289. package/src/providers/cursor-pi-args.ts +187 -0
  290. package/src/providers/cursor.d.ts +72 -0
  291. package/src/providers/cursor.ts +3396 -0
  292. package/src/providers/dashscope-token-plan-headers.d.ts +57 -0
  293. package/src/providers/dashscope-token-plan-headers.ts +84 -0
  294. package/src/providers/error-message.d.ts +27 -0
  295. package/src/providers/error-message.ts +21 -0
  296. package/src/providers/github-copilot-headers.d.ts +40 -0
  297. package/src/providers/github-copilot-headers.ts +140 -0
  298. package/src/providers/gitlab-duo.d.ts +27 -0
  299. package/src/providers/gitlab-duo.ts +393 -0
  300. package/src/providers/google-auth.d.ts +26 -0
  301. package/src/providers/google-auth.ts +262 -0
  302. package/src/providers/google-gemini-cli.d.ts +75 -0
  303. package/src/providers/google-gemini-cli.ts +969 -0
  304. package/src/providers/google-gemini-headers.d.ts +43 -0
  305. package/src/providers/google-gemini-headers.ts +100 -0
  306. package/src/providers/google-shared.d.ts +183 -0
  307. package/src/providers/google-shared.ts +1105 -0
  308. package/src/providers/google-types.d.ts +138 -0
  309. package/src/providers/google-types.ts +167 -0
  310. package/src/providers/google-vertex.d.ts +11 -0
  311. package/src/providers/google-vertex.ts +124 -0
  312. package/src/providers/google.d.ts +4 -0
  313. package/src/providers/google.ts +41 -0
  314. package/src/providers/grammar.d.ts +1 -0
  315. package/src/providers/grammar.ts +70 -0
  316. package/src/providers/kimi.d.ts +27 -0
  317. package/src/providers/kimi.ts +52 -0
  318. package/src/providers/kiro-api-key.d.ts +50 -0
  319. package/src/providers/kiro-api-key.ts +786 -0
  320. package/src/providers/kiro-codewhisperer.d.ts +11 -0
  321. package/src/providers/kiro-codewhisperer.ts +600 -0
  322. package/src/providers/mock.ts +526 -0
  323. package/src/providers/ollama.d.ts +41 -0
  324. package/src/providers/ollama.ts +645 -0
  325. package/src/providers/openai-anthropic-shim.d.ts +31 -0
  326. package/src/providers/openai-anthropic-shim.ts +156 -0
  327. package/src/providers/openai-bounded-rate-limits.d.ts +3 -0
  328. package/src/providers/openai-bounded-rate-limits.ts +57 -0
  329. package/src/providers/openai-chat-server-schema.ts +254 -0
  330. package/src/providers/openai-chat-server.ts +724 -0
  331. package/src/providers/openai-codex/constants.d.ts +26 -0
  332. package/src/providers/openai-codex/constants.ts +43 -0
  333. package/src/providers/openai-codex/request-transformer.d.ts +50 -0
  334. package/src/providers/openai-codex/request-transformer.ts +219 -0
  335. package/src/providers/openai-codex/response-handler.d.ts +18 -0
  336. package/src/providers/openai-codex/response-handler.ts +111 -0
  337. package/src/providers/openai-codex-responses.d.ts +71 -0
  338. package/src/providers/openai-codex-responses.ts +3288 -0
  339. package/src/providers/openai-completions-compat.d.ts +6 -0
  340. package/src/providers/openai-completions-compat.ts +6 -0
  341. package/src/providers/openai-completions.d.ts +35 -0
  342. package/src/providers/openai-completions.ts +2294 -0
  343. package/src/providers/openai-opencodex-responses.ts +174 -0
  344. package/src/providers/openai-request-transform.d.ts +4 -0
  345. package/src/providers/openai-request-transform.ts +136 -0
  346. package/src/providers/openai-responses-server-schema.ts +290 -0
  347. package/src/providers/openai-responses-server.ts +1268 -0
  348. package/src/providers/openai-responses-shared.d.ts +106 -0
  349. package/src/providers/openai-responses-shared.ts +1253 -0
  350. package/src/providers/openai-responses.d.ts +37 -0
  351. package/src/providers/openai-responses.ts +989 -0
  352. package/src/providers/pi-native-client.d.ts +29 -0
  353. package/src/providers/pi-native-client.ts +243 -0
  354. package/src/providers/pi-native-server.ts +488 -0
  355. package/src/providers/register-builtins.d.ts +59 -0
  356. package/src/providers/register-builtins.ts +544 -0
  357. package/src/providers/synthetic.d.ts +26 -0
  358. package/src/providers/synthetic.ts +50 -0
  359. package/src/providers/transform-messages.d.ts +33 -0
  360. package/src/providers/transform-messages.ts +408 -0
  361. package/src/providers/vision-guard.d.ts +8 -0
  362. package/src/providers/vision-guard.ts +31 -0
  363. package/src/rate-limit-utils.d.ts +19 -0
  364. package/src/rate-limit-utils.ts +102 -0
  365. package/src/stream.d.ts +45 -0
  366. package/src/stream.ts +1306 -0
  367. package/src/types.d.ts +1073 -0
  368. package/src/types.ts +1305 -0
  369. package/src/usage/claude.ts +449 -0
  370. package/src/usage/gemini.ts +250 -0
  371. package/src/usage/github-copilot.ts +421 -0
  372. package/src/usage/google-antigravity.ts +201 -0
  373. package/src/usage/grok-cli.ts +259 -0
  374. package/src/usage/kimi.ts +285 -0
  375. package/src/usage/minimax-code.ts +31 -0
  376. package/src/usage/openai-codex.ts +503 -0
  377. package/src/usage/shared.ts +10 -0
  378. package/src/usage/zai.ts +247 -0
  379. package/src/usage.ts +190 -0
  380. package/src/utils/abort.d.ts +19 -0
  381. package/src/utils/abort.ts +51 -0
  382. package/src/utils/anthropic-auth.ts +95 -0
  383. package/src/utils/block-symbols.d.ts +6 -0
  384. package/src/utils/block-symbols.ts +11 -0
  385. package/src/utils/discovery/antigravity.ts +275 -0
  386. package/src/utils/discovery/codex.ts +362 -0
  387. package/src/utils/discovery/cursor.ts +388 -0
  388. package/src/utils/discovery/gemini.ts +248 -0
  389. package/src/utils/discovery/index.ts +4 -0
  390. package/src/utils/discovery/openai-compatible.ts +379 -0
  391. package/src/utils/event-stream.d.ts +41 -0
  392. package/src/utils/event-stream.ts +269 -0
  393. package/src/utils/fallback-transport.d.ts +110 -0
  394. package/src/utils/fallback-transport.ts +411 -0
  395. package/src/utils/fireworks-model-id.d.ts +10 -0
  396. package/src/utils/fireworks-model-id.ts +30 -0
  397. package/src/utils/foundry.d.ts +11 -0
  398. package/src/utils/foundry.ts +18 -0
  399. package/src/utils/h2-fetch.ts +60 -0
  400. package/src/utils/http-inspector.d.ts +59 -0
  401. package/src/utils/http-inspector.ts +380 -0
  402. package/src/utils/idle-iterator.d.ts +122 -0
  403. package/src/utils/idle-iterator.ts +410 -0
  404. package/src/utils/json-parse.d.ts +98 -0
  405. package/src/utils/json-parse.ts +607 -0
  406. package/src/utils/oauth/alibaba-token-plan.ts +60 -0
  407. package/src/utils/oauth/anthropic.ts +233 -0
  408. package/src/utils/oauth/api-key-login.ts +98 -0
  409. package/src/utils/oauth/api-key-validation.ts +344 -0
  410. package/src/utils/oauth/bizrouter.ts +15 -0
  411. package/src/utils/oauth/callback-server.d.ts +80 -0
  412. package/src/utils/oauth/callback-server.ts +359 -0
  413. package/src/utils/oauth/cerebras.ts +16 -0
  414. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  415. package/src/utils/oauth/commandcode.ts +17 -0
  416. package/src/utils/oauth/cursor.ts +157 -0
  417. package/src/utils/oauth/deepinfra.ts +15 -0
  418. package/src/utils/oauth/deepseek.ts +53 -0
  419. package/src/utils/oauth/firepass.ts +24 -0
  420. package/src/utils/oauth/fireworks.ts +15 -0
  421. package/src/utils/oauth/fugu.ts +15 -0
  422. package/src/utils/oauth/github-copilot.d.ts +38 -0
  423. package/src/utils/oauth/github-copilot.ts +362 -0
  424. package/src/utils/oauth/gitlab-duo.ts +123 -0
  425. package/src/utils/oauth/glm-zcode.d.ts +71 -0
  426. package/src/utils/oauth/glm-zcode.ts +433 -0
  427. package/src/utils/oauth/google-antigravity.ts +200 -0
  428. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  429. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  430. package/src/utils/oauth/huggingface.ts +62 -0
  431. package/src/utils/oauth/index.ts +558 -0
  432. package/src/utils/oauth/kagi.ts +47 -0
  433. package/src/utils/oauth/kilo.ts +87 -0
  434. package/src/utils/oauth/kimi.d.ts +17 -0
  435. package/src/utils/oauth/kimi.ts +275 -0
  436. package/src/utils/oauth/kiro.ts +448 -0
  437. package/src/utils/oauth/litellm.ts +47 -0
  438. package/src/utils/oauth/lm-studio.ts +38 -0
  439. package/src/utils/oauth/mara.ts +16 -0
  440. package/src/utils/oauth/minimax-code.ts +78 -0
  441. package/src/utils/oauth/moonshot.ts +16 -0
  442. package/src/utils/oauth/nanogpt.ts +15 -0
  443. package/src/utils/oauth/nvidia.ts +70 -0
  444. package/src/utils/oauth/oauth.html +199 -0
  445. package/src/utils/oauth/ollama-cloud.ts +28 -0
  446. package/src/utils/oauth/ollama.ts +47 -0
  447. package/src/utils/oauth/openai-codex.ts +299 -0
  448. package/src/utils/oauth/opencode.ts +49 -0
  449. package/src/utils/oauth/opengateway.ts +15 -0
  450. package/src/utils/oauth/openrouter.ts +16 -0
  451. package/src/utils/oauth/parallel.ts +46 -0
  452. package/src/utils/oauth/perplexity.ts +225 -0
  453. package/src/utils/oauth/pkce.ts +18 -0
  454. package/src/utils/oauth/qianfan.ts +58 -0
  455. package/src/utils/oauth/qwen-portal.ts +60 -0
  456. package/src/utils/oauth/sglang.ts +42 -0
  457. package/src/utils/oauth/synthetic.ts +15 -0
  458. package/src/utils/oauth/tavily.ts +46 -0
  459. package/src/utils/oauth/together.ts +16 -0
  460. package/src/utils/oauth/types.d.ts +56 -0
  461. package/src/utils/oauth/types.ts +122 -0
  462. package/src/utils/oauth/venice.ts +59 -0
  463. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  464. package/src/utils/oauth/vllm.ts +42 -0
  465. package/src/utils/oauth/xai.ts +246 -0
  466. package/src/utils/oauth/xiaomi.ts +199 -0
  467. package/src/utils/oauth/zai.ts +60 -0
  468. package/src/utils/oauth/zenmux.ts +15 -0
  469. package/src/utils/overflow.ts +275 -0
  470. package/src/utils/parse-bind.ts +81 -0
  471. package/src/utils/provider-response.d.ts +6 -0
  472. package/src/utils/provider-response.ts +30 -0
  473. package/src/utils/provider-safety-stop.ts +8 -0
  474. package/src/utils/proxy.d.ts +7 -0
  475. package/src/utils/proxy.ts +652 -0
  476. package/src/utils/retry-after.d.ts +3 -0
  477. package/src/utils/retry-after.ts +110 -0
  478. package/src/utils/retry-budget.d.ts +1 -0
  479. package/src/utils/retry-budget.ts +4 -0
  480. package/src/utils/retry.d.ts +29 -0
  481. package/src/utils/retry.ts +67 -0
  482. package/src/utils/schema/CONSTRAINTS.md +164 -0
  483. package/src/utils/schema/adapt.d.ts +24 -0
  484. package/src/utils/schema/adapt.ts +36 -0
  485. package/src/utils/schema/compatibility.d.ts +30 -0
  486. package/src/utils/schema/compatibility.ts +435 -0
  487. package/src/utils/schema/dereference.d.ts +11 -0
  488. package/src/utils/schema/dereference.ts +98 -0
  489. package/src/utils/schema/draft.d.ts +10 -0
  490. package/src/utils/schema/draft.ts +341 -0
  491. package/src/utils/schema/equality.d.ts +4 -0
  492. package/src/utils/schema/equality.ts +97 -0
  493. package/src/utils/schema/fields.d.ts +49 -0
  494. package/src/utils/schema/fields.ts +190 -0
  495. package/src/utils/schema/index.d.ts +14 -0
  496. package/src/utils/schema/index.ts +14 -0
  497. package/src/utils/schema/json-schema-validator.d.ts +12 -0
  498. package/src/utils/schema/json-schema-validator.ts +577 -0
  499. package/src/utils/schema/meta-validator.d.ts +2 -0
  500. package/src/utils/schema/meta-validator.ts +167 -0
  501. package/src/utils/schema/normalize.d.ts +93 -0
  502. package/src/utils/schema/normalize.ts +1588 -0
  503. package/src/utils/schema/root-combinator.d.ts +12 -0
  504. package/src/utils/schema/root-combinator.ts +143 -0
  505. package/src/utils/schema/spill.d.ts +8 -0
  506. package/src/utils/schema/spill.ts +43 -0
  507. package/src/utils/schema/stamps.d.ts +25 -0
  508. package/src/utils/schema/stamps.ts +97 -0
  509. package/src/utils/schema/types.d.ts +4 -0
  510. package/src/utils/schema/types.ts +11 -0
  511. package/src/utils/schema/wire.d.ts +54 -0
  512. package/src/utils/schema/wire.ts +213 -0
  513. package/src/utils/schema/zod-decontaminate.d.ts +31 -0
  514. package/src/utils/schema/zod-decontaminate.ts +331 -0
  515. package/src/utils/sse-debug.d.ts +10 -0
  516. package/src/utils/sse-debug.ts +289 -0
  517. package/src/utils/tool-call-healing.d.ts +80 -0
  518. package/src/utils/tool-call-healing.ts +298 -0
  519. package/src/utils/tool-choice-capability.d.ts +57 -0
  520. package/src/utils/tool-choice-capability.ts +633 -0
  521. package/src/utils/tool-choice.d.ts +50 -0
  522. package/src/utils/tool-choice.ts +99 -0
  523. package/src/utils/validation.ts +1080 -0
  524. package/src/utils.d.ts +117 -0
  525. package/src/utils.ts +523 -0
@@ -0,0 +1,1315 @@
1
+ /**
2
+ * vib auth-gateway HTTP server.
3
+ *
4
+ * Accepts a provider-scoped provider-format request (OpenAI chat-completions, Anthropic
5
+ * messages, OpenAI Responses) and dispatches through pi-ai's `streamSimple()`
6
+ * — which handles credential injection, anthropic-beta headers, OpenAI code backend
7
+ * websocket transport, and all the per-provider intricacies. The gateway is
8
+ * pure protocol translation: foreign wire → vib Context → pi-ai stream() →
9
+ * vib events → foreign wire.
10
+ *
11
+ * Endpoints:
12
+ * GET /healthz → unauth; ok + version
13
+ * GET /v1/usage → aggregated provider usage (5-min per-credential cache via AuthStorage)
14
+ * GET /v1/credentials/check → per-credential auth probe (diagnose 401s in a multi-account pool)
15
+ * GET /v1/models → list models from the selected provider scope
16
+ * POST /v1/chat/completions → OpenAI chat-completions in/out
17
+ * POST /v1/messages → Anthropic messages in/out
18
+ * POST /v1/responses → OpenAI Responses in/out
19
+ */
20
+
21
+ import { logger } from "@vib-rato/utils";
22
+ import { cleanReason } from "../auth-broker/redact";
23
+ import type { AuthStorage } from "../auth-storage";
24
+ import { Effort } from "../model-thinking";
25
+ import * as anthropicMessages from "../providers/anthropic-messages-server";
26
+ import * as openaiChat from "../providers/openai-chat-server";
27
+ import * as openaiResponses from "../providers/openai-responses-server";
28
+ import * as piNative from "../providers/pi-native-server";
29
+ import { streamSimple } from "../stream";
30
+ import type {
31
+ Api,
32
+ AssistantMessage,
33
+ AssistantMessageEvent,
34
+ AssistantMessageEventStream,
35
+ AuthRetryCredential,
36
+ Context,
37
+ Model,
38
+ Provider,
39
+ SimpleStreamOptions,
40
+ } from "../types";
41
+ import { beginAttempt, classifyFallbackTrigger } from "../utils/fallback-transport";
42
+ import { assertAuthenticatedOrLoopback, parseBind } from "../utils/parse-bind";
43
+ import {
44
+ captureRequestHeaders,
45
+ corsHeaders,
46
+ isAuthorized,
47
+ isNoAuthBrowserOriginRequest,
48
+ json,
49
+ resolvePeer,
50
+ withCors,
51
+ } from "./http";
52
+ import type {
53
+ AuthGatewayServerHandle,
54
+ AuthGatewayServerOptions,
55
+ AuthGatewayFormatModule as FormatModule,
56
+ AuthGatewayParsedRequest as ParsedFormatRequest,
57
+ } from "./types";
58
+ import { AUTH_GATEWAY_PROVIDER_APIS, DEFAULT_AUTH_GATEWAY_BIND } from "./types";
59
+
60
+ // ParsedFormatRequest / ParsedFormatOptions / FormatModule come from ./types.
61
+
62
+ export type ModelResolver = (modelId: string) => Model<Api> | undefined;
63
+
64
+ export interface AuthGatewayBootOptions extends AuthGatewayServerOptions {
65
+ /** Source of credentials. Caller wires this to a broker-backed AuthStorage. */
66
+ storage: AuthStorage;
67
+ /**
68
+ * Current broker-backed scope authority. When supplied, this is checked on
69
+ * every request so a live broker snapshot removal immediately fails closed.
70
+ */
71
+ hasProviderCredential: () => boolean;
72
+ /** Refresh the dispatch cache from the current broker snapshot before use. */
73
+ reloadProviderCredentials: (signal?: AbortSignal) => Promise<void>;
74
+ /** Confirm that the selected key is still present in the current authority snapshot. */
75
+ validateProviderCredential: (provider: string, apiKey: string) => boolean;
76
+ /**
77
+ * Resolve a client-requested model id to a pi-ai Model. Caller supplies
78
+ * this from a ModelRegistry (lives in `coding-agent` to avoid an inverse
79
+ * dependency in `pi-ai`).
80
+ */
81
+ resolveModel: ModelResolver;
82
+ /** Supplier for the source-backed model catalog used by `/v1/models`. */
83
+ listModels: () => Iterable<Model<Api>>;
84
+ }
85
+
86
+ export interface AuthGatewayModelCatalog {
87
+ readonly models: readonly Model<Api>[];
88
+ resolve(modelId: string): Model<Api> | undefined;
89
+ }
90
+
91
+ function modelApiForProvider(provider: Provider): Api | undefined {
92
+ return AUTH_GATEWAY_PROVIDER_APIS[provider];
93
+ }
94
+
95
+ /**
96
+ * Whether a model can be served through the broker-backed auth gateway.
97
+ *
98
+ * Bedrock's credential chain is process-local AWS authority, not a broker
99
+ * credential. Advertising a native Bedrock model from this gateway would let
100
+ * direct callers bypass the broker boundary (and make readiness lie about a
101
+ * model the gateway cannot authenticate). Keep this predicate shared with the
102
+ * CLI readiness checks so every entry point applies the same fence.
103
+ */
104
+ export function isAuthGatewayModelBrokerConsumable(model: Pick<Model<Api>, "api" | "transport">): boolean {
105
+ return (
106
+ model.api !== "bedrock-converse-stream" &&
107
+ model.api !== "google-vertex" &&
108
+ model.api !== "kiro-codewhisperer-stream" &&
109
+ model.transport !== "pi-native"
110
+ );
111
+ }
112
+
113
+ function isModelInProviderScope(model: Model<Api>, provider: Provider): boolean {
114
+ if (model.provider !== provider) return false;
115
+ const expectedApi = modelApiForProvider(provider);
116
+ return expectedApi === undefined || model.api === expectedApi;
117
+ }
118
+
119
+ /**
120
+ * Build an unambiguous, provider-scoped catalog.
121
+ *
122
+ * Models from other providers are intentionally ignored rather than allowed
123
+ * to compete for the same id. Duplicate ids within the selected provider are
124
+ * rejected because choosing either one would make request dispatch
125
+ * order-dependent.
126
+ */
127
+ export function createAuthGatewayModelCatalog(
128
+ provider: Provider,
129
+ models: Iterable<Model<Api>>,
130
+ ): AuthGatewayModelCatalog {
131
+ const byId = new Map<string, Model<Api>>();
132
+ for (const model of models) {
133
+ if (!isAuthGatewayModelBrokerConsumable(model)) continue;
134
+ if (!isModelInProviderScope(model, provider)) continue;
135
+ if (byId.has(model.id)) {
136
+ throw new Error(`Ambiguous auth-gateway model id ${model.id} for provider ${provider}`);
137
+ }
138
+ byId.set(model.id, model);
139
+ }
140
+ const scopedModels = [...byId.values()];
141
+ return {
142
+ models: scopedModels,
143
+ resolve: (modelId: string) => byId.get(modelId),
144
+ };
145
+ }
146
+
147
+ function resolveScopedModel(
148
+ opts: AuthGatewayBootOptions,
149
+ catalog: AuthGatewayModelCatalog,
150
+ modelId: string,
151
+ ): Model<Api> | undefined {
152
+ const catalogModel = catalog.resolve(modelId);
153
+ if (!catalogModel) return undefined;
154
+ const resolved = opts.resolveModel(modelId);
155
+ if (
156
+ !resolved ||
157
+ resolved !== catalogModel ||
158
+ resolved.id !== catalogModel.id ||
159
+ resolved.api !== catalogModel.api ||
160
+ !isModelInProviderScope(resolved, opts.providerScope.provider)
161
+ ) {
162
+ return undefined;
163
+ }
164
+ return catalogModel;
165
+ }
166
+
167
+ function hasProviderCredential(opts: AuthGatewayBootOptions): boolean {
168
+ return opts.hasProviderCredential();
169
+ }
170
+
171
+ type ProviderScopeAvailability = "available" | "absent" | "reload_failed";
172
+
173
+ async function providerScopeAvailability(
174
+ opts: AuthGatewayBootOptions,
175
+ signal?: AbortSignal,
176
+ ): Promise<ProviderScopeAvailability> {
177
+ try {
178
+ await opts.reloadProviderCredentials(signal);
179
+ } catch (error) {
180
+ logger.warn("auth-gateway provider snapshot reload failed", {
181
+ provider: opts.providerScope.provider,
182
+ error: cleanReason(error) ?? "snapshot reload failed",
183
+ });
184
+ return "reload_failed";
185
+ }
186
+ return hasProviderCredential(opts) ? "available" : "absent";
187
+ }
188
+
189
+ class GatewayCredentialError extends Error {
190
+ readonly status: number;
191
+ readonly type: string;
192
+
193
+ constructor(status: number, type: string, message: string) {
194
+ super(message);
195
+ this.name = "GatewayCredentialError";
196
+ this.status = status;
197
+ this.type = type;
198
+ }
199
+ }
200
+
201
+ const credentialAuthorityTails = new WeakMap<AuthGatewayBootOptions, Promise<void>>();
202
+
203
+ interface GatewayCredentialLease {
204
+ apiKey: string;
205
+ release(): void;
206
+ }
207
+
208
+ export function releaseGatewayCredentialLeaseOnAdmission(
209
+ events: Pick<AssistantMessageEventStream, "result">,
210
+ release: () => void,
211
+ signal?: AbortSignal,
212
+ ): void {
213
+ let released = false;
214
+ const releaseOnce = (): void => {
215
+ if (released) return;
216
+ released = true;
217
+ signal?.removeEventListener("abort", releaseOnce);
218
+ release();
219
+ };
220
+ if (signal?.aborted) {
221
+ releaseOnce();
222
+ return;
223
+ }
224
+ signal?.addEventListener("abort", releaseOnce, { once: true });
225
+ // A deferred provider import can fail before the admission hook runs. Do
226
+ // not strand the authority lease in that case, but never wait for a
227
+ // successful stream's full response lifetime.
228
+ void events.result().then(releaseOnce, releaseOnce);
229
+ }
230
+
231
+ async function resolveGatewayApiKey(
232
+ opts: AuthGatewayBootOptions,
233
+ model: Model<Api>,
234
+ peer: string,
235
+ signal: AbortSignal,
236
+ ): Promise<string> {
237
+ try {
238
+ const scopeAvailability = await providerScopeAvailability(opts, signal);
239
+ if (scopeAvailability === "reload_failed") {
240
+ throw new GatewayCredentialError(503, "upstream_error", "Auth broker unavailable");
241
+ }
242
+ if (scopeAvailability !== "available") {
243
+ throw new GatewayCredentialError(
244
+ 401,
245
+ "authentication_error",
246
+ `No credential available for provider ${model.provider}`,
247
+ );
248
+ }
249
+ const apiKey = await opts.storage.getApiKey(model.provider, undefined, { modelId: model.id, signal });
250
+ if (!apiKey || !opts.validateProviderCredential(model.provider, apiKey) || !hasProviderCredential(opts)) {
251
+ throw new GatewayCredentialError(
252
+ 401,
253
+ "authentication_error",
254
+ `No credential available for provider ${model.provider}`,
255
+ );
256
+ }
257
+ return apiKey;
258
+ } catch (error) {
259
+ if (error instanceof GatewayCredentialError) throw error;
260
+ const classified = classifyGatewayError(error);
261
+ logger.warn("auth-gateway getApiKey threw", {
262
+ provider: model.provider,
263
+ peer,
264
+ error: classified.message,
265
+ });
266
+ throw new GatewayCredentialError(classified.status, classified.type, classified.message);
267
+ }
268
+ }
269
+
270
+ async function acquireGatewayApiKey(
271
+ opts: AuthGatewayBootOptions,
272
+ model: Model<Api>,
273
+ peer: string,
274
+ signal: AbortSignal,
275
+ ): Promise<GatewayCredentialLease> {
276
+ const previous = credentialAuthorityTails.get(opts) ?? Promise.resolve();
277
+ const deferred = Promise.withResolvers<void>();
278
+ const tail = previous.then(
279
+ () => deferred.promise,
280
+ () => deferred.promise,
281
+ );
282
+ credentialAuthorityTails.set(opts, tail);
283
+ let released = false;
284
+ const release = (): void => {
285
+ if (released) return;
286
+ released = true;
287
+ deferred.resolve();
288
+ };
289
+ void tail.then(() => {
290
+ if (credentialAuthorityTails.get(opts) === tail) credentialAuthorityTails.delete(opts);
291
+ });
292
+ try {
293
+ if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
294
+ const abort = Promise.withResolvers<never>();
295
+ const onAbort = (): void =>
296
+ abort.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
297
+ signal.addEventListener("abort", onAbort, { once: true });
298
+ try {
299
+ await Promise.race([previous, abort.promise]);
300
+ } finally {
301
+ signal.removeEventListener("abort", onAbort);
302
+ }
303
+ const apiKey = await resolveGatewayApiKey(opts, model, peer, signal);
304
+ const dispatchTicket = await opts.storage.acquireCredentialDispatchTicket?.(model.provider, signal);
305
+ if (!opts.validateProviderCredential(model.provider, apiKey) || !hasProviderCredential(opts)) {
306
+ dispatchTicket?.release();
307
+ throw new GatewayCredentialError(
308
+ 401,
309
+ "authentication_error",
310
+ `No credential available for provider ${model.provider}`,
311
+ );
312
+ }
313
+ return {
314
+ apiKey,
315
+ release: () => {
316
+ // The store-owned ticket orders remote snapshot authority against
317
+ // provider admission; the gateway tail independently orders local
318
+ // acquisitions without serializing response lifetimes.
319
+ dispatchTicket?.release();
320
+ release();
321
+ },
322
+ };
323
+ } catch (error) {
324
+ release();
325
+ if (error instanceof GatewayCredentialError) throw error;
326
+ throw error;
327
+ }
328
+ }
329
+
330
+ // `parseBind` lives in ../utils/parse-bind so the gateway and broker can't
331
+ // drift on accepted inputs (e.g. empty hostname, IPv6 brackets).
332
+
333
+ const FORMAT_ROUTES: Record<string, { module: FormatModule; label: string }> = {
334
+ "/v1/chat/completions": { module: openaiChat, label: "openai-chat" },
335
+ "/v1/messages": { module: anthropicMessages, label: "anthropic-messages" },
336
+ "/v1/responses": { module: openaiResponses, label: "openai-responses" },
337
+ };
338
+
339
+ // (passthrough fast-path removed — it bypassed pi-ai provider logic, in
340
+ // particular the Anthropic Anthropic-code OAuth system-prompt prefix injection.
341
+ // Every request now takes the translate path so credential-specific request
342
+ // shaping always applies.)
343
+
344
+ // Options the caller's wire format may carry but the resolved provider can't
345
+ // honour are dropped silently in `buildStreamOptions`. We used to 400 here
346
+ // (`Unsupported option: temperature for OpenAI code provider-responses`), but every
347
+ // realistic client (llm-git, openai SDK, anthropic SDK) bakes some of these
348
+ // defaults in without knowing which model they'll resolve to. Failing loudly
349
+ // just turned that into per-call config hell. Silent strip is what the
350
+ // upstream provider would do anyway when it ignores extra fields.
351
+
352
+ /**
353
+ * Derive a stable cache identity from the parts of the request that don't
354
+ * change turn-to-turn within a logical conversation: model id, system prompt,
355
+ * tool definitions, and the first message (the conversation seed). OpenAI code backend-class
356
+ * backends only cache prefixes when an explicit `prompt_cache_key` is set;
357
+ * without one, two requests with the same prefix but different trailing
358
+ * messages don't coalesce. This bridges Anthropic-style clients (which signal
359
+ * caching via `cache_control` markers rather than an opaque key) to OpenAI code backend's
360
+ * keyed model so cross-protocol caching "just works".
361
+ *
362
+ * Including the first message scopes the key to one logical conversation:
363
+ * two different chats with the same system prompt no longer share a cache
364
+ * bucket and can't trample each other's prefix-tree entries.
365
+ *
366
+ * Anthropic-backed requests ignore `sessionId`; the key is harmless there.
367
+ */
368
+ function deriveSessionId(modelId: string, context: Context): string {
369
+ const parts: string[] = [modelId];
370
+ if (context.systemPrompt && context.systemPrompt.length > 0) {
371
+ parts.push(context.systemPrompt.join("\n\n"));
372
+ }
373
+ if (context.tools && context.tools.length > 0) {
374
+ parts.push(JSON.stringify(context.tools));
375
+ }
376
+ const first = context.messages?.[0];
377
+ if (first) {
378
+ // Strip timestamp / provider metadata so the hash is stable across turns
379
+ // of the same conversation (vib re-stamps every parsed Message). role +
380
+ // content is what's actually on the wire.
381
+ parts.push(JSON.stringify({ role: first.role, content: first.content }));
382
+ }
383
+ const seed = parts.join("\u0000");
384
+ const hex = new Bun.CryptoHasher("sha256").update(seed).digest("hex");
385
+ // Format the leading 128 bits as a v4-shape UUID (8-4-4-4-12). OpenAI code backend's
386
+ // `normalizeOpenAIResponsesPromptCacheKey` accepts ≤64 chars verbatim, so
387
+ // the 36-char UUID flows through unchanged.
388
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
389
+ }
390
+
391
+ function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: AbortSignal): SimpleStreamOptions {
392
+ // Gateway authority is acquired once per managed attempt. Provider-internal
393
+ // retries would otherwise resend a captured credential after the broker lease
394
+ // is released; replacement attempts must flow through onAuthError instead.
395
+ const opts: SimpleStreamOptions = {
396
+ signal,
397
+ requestMaxRetries: 0,
398
+ streamMaxRetries: 0,
399
+ disableProviderRetries: true,
400
+ };
401
+ const { options } = parsed;
402
+ // OpenAI code backend backend rejects `temperature` / `top_p` (per-model defaults only),
403
+ // so we drop them silently for that one provider. Every other unsupported
404
+ // option is just ignored by `streamSimple` if the underlying provider
405
+ // doesn't honour it.
406
+ const isCodex = api === "openai-codex-responses";
407
+ if (options.maxOutputTokens !== undefined) opts.maxTokens = options.maxOutputTokens;
408
+ if (options.temperature !== undefined && !isCodex) opts.temperature = options.temperature;
409
+ if (options.topP !== undefined && !isCodex) opts.topP = options.topP;
410
+ if (options.topK !== undefined) opts.topK = options.topK;
411
+ if (options.minP !== undefined) opts.minP = options.minP;
412
+ if (options.stopSequences !== undefined) opts.stopSequences = options.stopSequences;
413
+ if (options.presencePenalty !== undefined) opts.presencePenalty = options.presencePenalty;
414
+ if (options.frequencyPenalty !== undefined) opts.frequencyPenalty = options.frequencyPenalty;
415
+ if (options.repetitionPenalty !== undefined) opts.repetitionPenalty = options.repetitionPenalty;
416
+ if (options.metadata !== undefined) opts.metadata = options.metadata;
417
+ if (options.headers !== undefined) opts.headers = { ...(opts.headers ?? {}), ...options.headers };
418
+ if (options.toolChoice !== undefined) {
419
+ opts.toolChoice =
420
+ typeof options.toolChoice === "object" ? { type: "tool", name: options.toolChoice.name } : options.toolChoice;
421
+ }
422
+ if (options.reasoning !== undefined) opts.reasoning = options.reasoning;
423
+ if (options.disableReasoning !== undefined) opts.disableReasoning = options.disableReasoning;
424
+ if (options.hideThinkingSummary !== undefined) opts.hideThinkingSummary = options.hideThinkingSummary;
425
+ if (options.serviceTier !== undefined) opts.serviceTier = options.serviceTier;
426
+ if (options.cacheRetention !== undefined) opts.cacheRetention = options.cacheRetention;
427
+ // Client-supplied `prompt_cache_key` wins; otherwise derive a stable
428
+ // key from the model + system + tools so prefix caching engages on
429
+ // OpenAI code backend-class backends across turns of the same logical conversation.
430
+ opts.sessionId = options.promptCacheKey ?? deriveSessionId(parsed.modelId, parsed.context);
431
+ if (options.thinkingBudgets) {
432
+ opts.thinkingBudgets = { ...(opts.thinkingBudgets ?? {}), ...options.thinkingBudgets };
433
+ }
434
+ if (options.explicitThinkingBudgetTokens !== undefined) {
435
+ // Mirror Rust's `resolve_thinking_budget`: explicit budget pins onto
436
+ // whichever effort the client requested (or High when unspecified) and
437
+ // ALSO sets the effort so providers that gate on `reasoning` actually
438
+ // surface the budget.
439
+ const effort = options.reasoning ?? Effort.High;
440
+ opts.thinkingBudgets = {
441
+ ...(opts.thinkingBudgets ?? {}),
442
+ [effort]: options.explicitThinkingBudgetTokens,
443
+ };
444
+ opts.reasoning ??= effort;
445
+ }
446
+ // Fields that don't yet have a matching pi-ai `SimpleStreamOptions` slot.
447
+ // Surfaced once in debug logs so they show up when wiring a new provider,
448
+ // but NEVER widened into `options.extra` — every consumer would have to
449
+ // re-implement the typed parse to read them back out.
450
+ // TODO(pi-ai): land first-class fields and replace these blocks.
451
+ if (
452
+ options.parallelToolCalls !== undefined ||
453
+ options.previousResponseId !== undefined ||
454
+ options.seed !== undefined ||
455
+ options.logitBias !== undefined ||
456
+ options.user !== undefined ||
457
+ options.responseFormat !== undefined
458
+ ) {
459
+ logger.debug("auth-gateway dropped unsupported typed options", {
460
+ api,
461
+ parallelToolCalls: options.parallelToolCalls,
462
+ previousResponseId: options.previousResponseId,
463
+ seed: options.seed,
464
+ hasLogitBias: options.logitBias !== undefined,
465
+ hasUser: options.user !== undefined,
466
+ hasResponseFormat: options.responseFormat !== undefined,
467
+ });
468
+ }
469
+ return opts;
470
+ }
471
+
472
+ /**
473
+ * Classify an upstream / gateway-internal error into a status code and a
474
+ * provider-style error type tag. Used by `handleFormatEndpoint` /
475
+ * `handlePassthrough` to drive `route.module.formatError` so every wire
476
+ * format emits its native envelope shape.
477
+ */
478
+ function classifyGatewayError(err: unknown): { status: number; type: string; message: string } {
479
+ const rawMessage = err instanceof Error ? err.message : String(err);
480
+ const message = cleanReason(rawMessage) ?? "Upstream request failed";
481
+ const lower = rawMessage.toLowerCase();
482
+
483
+ // Custom pi-ai errors may attach a numeric `status` property; honor it
484
+ // when present and pick the matching tag.
485
+ const statusProp =
486
+ typeof err === "object" && err !== null && typeof (err as { status?: unknown }).status === "number"
487
+ ? (err as { status: number }).status | 0
488
+ : undefined;
489
+ if (statusProp !== undefined) {
490
+ if (statusProp === 401 || statusProp === 403)
491
+ return { status: statusProp, type: "authentication_error", message };
492
+ if (statusProp === 429) return { status: 429, type: "rate_limit_error", message };
493
+ if (statusProp >= 400 && statusProp < 500) return { status: statusProp, type: "invalid_request_error", message };
494
+ if (statusProp >= 500) return { status: statusProp, type: "upstream_error", message };
495
+ }
496
+
497
+ if (err instanceof Error && err.name === "AbortError") return { status: 499, type: "request_aborted", message };
498
+ if (lower.includes("aborted") || lower.includes("abortsignal")) {
499
+ return { status: 499, type: "request_aborted", message };
500
+ }
501
+ if (
502
+ lower.includes("401") ||
503
+ lower.includes("403") ||
504
+ lower.includes("unauthorized") ||
505
+ lower.includes("forbidden")
506
+ ) {
507
+ return { status: 401, type: "authentication_error", message };
508
+ }
509
+ if (lower.includes("429") || lower.includes("rate") || lower.includes("quota")) {
510
+ return { status: 429, type: "rate_limit_error", message };
511
+ }
512
+ if (lower.includes("unsupported") || lower.includes("invalid")) {
513
+ return { status: 400, type: "invalid_request_error", message };
514
+ }
515
+ return { status: 502, type: "upstream_error", message };
516
+ }
517
+
518
+ function redactGatewayError(error: unknown): Error {
519
+ const redacted = new Error(cleanReason(error) ?? "Upstream request failed");
520
+ if (error instanceof Error) {
521
+ redacted.name = error.name;
522
+ const status = (error as { status?: unknown }).status;
523
+ if (typeof status === "number") (redacted as Error & { status: number }).status = status;
524
+ }
525
+ return redacted;
526
+ }
527
+
528
+ function redactGatewayMessage(message: AssistantMessage): AssistantMessage {
529
+ if (message.errorMessage === undefined) return message;
530
+ return { ...message, errorMessage: cleanReason(message.errorMessage) ?? "Upstream request failed" };
531
+ }
532
+
533
+ /**
534
+ * Protect all gateway SSE encoders from upstream error text. Provider wire
535
+ * modules format `error` events themselves, so sanitize at this boundary and
536
+ * also convert iterator failures to bounded errors before their catch blocks.
537
+ */
538
+ function redactGatewayStream(events: AssistantMessageEventStream): AssistantMessageEventStream {
539
+ async function* redactedEvents(): AsyncGenerator<AssistantMessageEvent> {
540
+ try {
541
+ for await (const event of events) {
542
+ if (event.type === "error") yield { ...event, error: redactGatewayMessage(event.error) };
543
+ else yield event;
544
+ }
545
+ } catch (error) {
546
+ throw redactGatewayError(error);
547
+ }
548
+ }
549
+ const result = redactedEvents() as unknown as AssistantMessageEventStream;
550
+ result.result = async () => {
551
+ try {
552
+ return redactGatewayMessage(await events.result());
553
+ } catch (error) {
554
+ throw redactGatewayError(error);
555
+ }
556
+ };
557
+ return result;
558
+ }
559
+
560
+ async function refreshGatewayApiKeyAfterAuthError(
561
+ opts: AuthGatewayBootOptions,
562
+ model: Model<Api>,
563
+ provider: string,
564
+ oldKey: string,
565
+ error: unknown,
566
+ signal: AbortSignal,
567
+ format: string,
568
+ peer: string,
569
+ ): Promise<AuthRetryCredential | undefined> {
570
+ await opts.storage.invalidateCredentialMatching(provider, oldKey, signal);
571
+ logger.debug("auth-gateway retrying provider request after credential invalidation", {
572
+ format,
573
+ provider,
574
+ peer,
575
+ error: cleanReason(error) ?? "Upstream request failed",
576
+ });
577
+ try {
578
+ const lease = await acquireGatewayApiKey(opts, model, peer, signal);
579
+ return { apiKey: lease.apiKey, onStreamCreated: lease.release } satisfies AuthRetryCredential;
580
+ } catch (resolutionError) {
581
+ if (resolutionError instanceof GatewayCredentialError) {
582
+ logger.debug("auth-gateway has no broker-authorized replacement credential", {
583
+ format,
584
+ provider,
585
+ peer,
586
+ status: resolutionError.status,
587
+ });
588
+ return undefined;
589
+ }
590
+ throw resolutionError;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Records a managed gateway failure against the credential selected for this
596
+ * request. This deliberately never returns a replacement key: the outer
597
+ * fallback controller owns the next attempt and is the only component allowed
598
+ * to make another upstream request.
599
+ */
600
+ async function markManagedGatewayCredentialFailure(
601
+ storage: AuthStorage,
602
+ model: Model<Api>,
603
+ apiKey: string,
604
+ error: unknown,
605
+ signal: AbortSignal,
606
+ format: string,
607
+ peer: string,
608
+ ): Promise<void> {
609
+ const trigger = classifyFallbackTrigger(error);
610
+ try {
611
+ if (trigger.class === "auth" && trigger.authDisposition === "forbidden") {
612
+ // A plain `forbidden` is an authorization or configuration defect.
613
+ // Blocking the credential here would hide it and would cycle through
614
+ // every otherwise-healthy row in a multi-credential pool.
615
+ return;
616
+ }
617
+ if (trigger.class === "auth") {
618
+ await storage.invalidateCredentialMatching(model.provider, apiKey, signal);
619
+ } else if (trigger.class === "quota" || trigger.class === "rate_limit") {
620
+ await storage.markUsageLimitReached(model.provider, undefined, {
621
+ retryAfterMs: trigger.retryAfterMs,
622
+ signal,
623
+ });
624
+ } else {
625
+ return;
626
+ }
627
+ logger.debug("auth-gateway recorded managed credential failure", {
628
+ format,
629
+ provider: model.provider,
630
+ peer,
631
+ trigger: trigger.class,
632
+ });
633
+ } catch (markError) {
634
+ // Credential bookkeeping must not replace the upstream failure returned to
635
+ // the fallback controller.
636
+ logger.warn("auth-gateway failed to record managed credential failure", {
637
+ format,
638
+ provider: model.provider,
639
+ peer,
640
+ error: cleanReason(markError) ?? "Credential bookkeeping failed",
641
+ });
642
+ }
643
+ }
644
+
645
+ function observeManagedGatewayFailure(
646
+ events: AssistantMessageEventStream,
647
+ markFailure: (error: unknown) => Promise<void>,
648
+ ): AssistantMessageEventStream {
649
+ let marked = false;
650
+ const markOnce = async (error: unknown): Promise<void> => {
651
+ if (marked) return;
652
+ marked = true;
653
+ await markFailure(error);
654
+ };
655
+ async function* observed() {
656
+ try {
657
+ for await (const event of events) {
658
+ if (event.type === "error") {
659
+ await markOnce(event.error.transportFailure ?? { kind: "transport", status: event.error.errorStatus });
660
+ }
661
+ yield event;
662
+ }
663
+ } catch (error) {
664
+ await markOnce(error);
665
+ throw error;
666
+ }
667
+ }
668
+ const result = observed() as unknown as AssistantMessageEventStream;
669
+ result.result = async () => {
670
+ try {
671
+ const message = await events.result();
672
+ if (message.stopReason === "error") {
673
+ await markOnce(message.transportFailure ?? { kind: "transport", status: message.errorStatus });
674
+ }
675
+ return message;
676
+ } catch (error) {
677
+ await markOnce(error);
678
+ throw error;
679
+ }
680
+ };
681
+ return result;
682
+ }
683
+
684
+ function clientClosedResponse(route: { module: FormatModule }): Response {
685
+ return route.module.formatError(499, "request_aborted", "client closed request");
686
+ }
687
+
688
+ function mirrorRequestAbort(req: Request): AbortController {
689
+ const controller = new AbortController();
690
+ if (req.signal.aborted) {
691
+ controller.abort(req.signal.reason);
692
+ } else {
693
+ req.signal.addEventListener("abort", () => controller.abort(req.signal.reason), { once: true });
694
+ }
695
+ return controller;
696
+ }
697
+
698
+ // (handlePassthrough removed — see note above.)
699
+
700
+ async function handleFormatEndpoint(
701
+ route: { module: FormatModule; label: string },
702
+ bootOpts: AuthGatewayBootOptions,
703
+ catalog: AuthGatewayModelCatalog,
704
+ req: Request,
705
+ peer: string,
706
+ ): Promise<Response> {
707
+ const controller = mirrorRequestAbort(req);
708
+ if (controller.signal.aborted) return clientClosedResponse(route);
709
+
710
+ let body: unknown;
711
+ try {
712
+ body = await req.json();
713
+ } catch (error) {
714
+ if (controller.signal.aborted) return clientClosedResponse(route);
715
+ return route.module.formatError(
716
+ 400,
717
+ "invalid_request_error",
718
+ `Invalid JSON body: ${cleanReason(error) ?? "request body could not be parsed"}`,
719
+ );
720
+ }
721
+ if (controller.signal.aborted) return clientClosedResponse(route);
722
+
723
+ // All three supported wire formats put the model id on a top-level `model`
724
+ // field. Read it without running the full strict schema so the route can
725
+ // produce a coherent error envelope when the model id is missing.
726
+ const modelId =
727
+ typeof body === "object" && body !== null && typeof (body as { model?: unknown }).model === "string"
728
+ ? (body as { model: string }).model
729
+ : undefined;
730
+ if (!modelId) {
731
+ return route.module.formatError(400, "invalid_request_error", "Missing top-level `model` field");
732
+ }
733
+
734
+ const model = resolveScopedModel(bootOpts, catalog, modelId);
735
+ if (!model) {
736
+ return route.module.formatError(404, "invalid_request_error", `Unknown model: ${modelId}`);
737
+ }
738
+
739
+ // Parse + validate against the strict format schema, rebuild as vib's
740
+ // canonical Context, dispatch through pi-ai's streamSimple, encode the
741
+ // canonical event stream back to the inbound format. There is no
742
+ // passthrough fast-path — every request flows through pi-ai so that
743
+ // credential-specific request shaping (OAuth Anthropic-code prefix, beta
744
+ // headers, OpenAI code backend websocket transport, …) always applies.
745
+ let parsed: ParsedFormatRequest;
746
+ try {
747
+ parsed = route.module.parseRequest(body, req.headers);
748
+ } catch (error) {
749
+ if (controller.signal.aborted) return clientClosedResponse(route);
750
+ const message = cleanReason(error) ?? "Request validation failed";
751
+ return route.module.formatError(400, "invalid_request_error", message);
752
+ }
753
+ // Merge gateway-captured passthrough headers under the parser's own
754
+ // captures. Parsers that set `options.headers` themselves win (they may
755
+ // have stripped or normalized values); the gateway's allow-list fills in
756
+ // anything they didn't touch.
757
+ {
758
+ const captured = captureRequestHeaders(req.headers);
759
+ parsed.options.headers = { ...captured, ...(parsed.options.headers ?? {}) };
760
+ }
761
+ if (controller.signal.aborted) return clientClosedResponse(route);
762
+
763
+ const streamOpts = buildStreamOptions(parsed, model.api, controller.signal);
764
+ if (streamOpts.fallbackManaged) {
765
+ streamOpts.fallbackAttempt = beginAttempt(model.id, "auth-gateway");
766
+ } else {
767
+ streamOpts.onAuthError = (provider, oldKey, error) =>
768
+ refreshGatewayApiKeyAfterAuthError(
769
+ bootOpts,
770
+ model,
771
+ provider,
772
+ oldKey,
773
+ error,
774
+ controller.signal,
775
+ route.label,
776
+ peer,
777
+ );
778
+ }
779
+
780
+ logger.info("auth-gateway request", {
781
+ format: route.label,
782
+ model: parsed.modelId,
783
+ resolvedProvider: model.provider,
784
+ resolvedModel: model.id,
785
+ stream: parsed.stream,
786
+ peer,
787
+ });
788
+
789
+ let apiKey: string;
790
+ let credentialLease: GatewayCredentialLease;
791
+ try {
792
+ credentialLease = await acquireGatewayApiKey(bootOpts, model, peer, controller.signal);
793
+ apiKey = credentialLease.apiKey;
794
+ streamOpts.apiKey = apiKey;
795
+ } catch (error) {
796
+ if (controller.signal.aborted) return clientClosedResponse(route);
797
+ if (error instanceof GatewayCredentialError) {
798
+ return route.module.formatError(error.status, error.type, error.message);
799
+ }
800
+ throw error;
801
+ }
802
+
803
+ let events: AssistantMessageEventStream;
804
+ let releasedAtAdmission = false;
805
+ const releaseAtAdmission = (): void => {
806
+ if (releasedAtAdmission) return;
807
+ releasedAtAdmission = true;
808
+ credentialLease.release();
809
+ };
810
+ streamOpts.onStreamCreated = releaseAtAdmission;
811
+ try {
812
+ if (controller.signal.aborted) {
813
+ credentialLease.release();
814
+ return clientClosedResponse(route);
815
+ }
816
+ events = streamSimple(model, parsed.context, streamOpts);
817
+ } catch (error) {
818
+ credentialLease.release();
819
+ if (streamOpts.fallbackManaged) {
820
+ await markManagedGatewayCredentialFailure(
821
+ bootOpts.storage,
822
+ model,
823
+ apiKey,
824
+ error,
825
+ controller.signal,
826
+ route.label,
827
+ peer,
828
+ );
829
+ }
830
+ const classified = classifyGatewayError(error);
831
+ logger.warn("auth-gateway streamSimple threw", { format: route.label, error: classified.message, peer });
832
+ return route.module.formatError(classified.status, classified.type, classified.message);
833
+ }
834
+ releaseGatewayCredentialLeaseOnAdmission(events, releaseAtAdmission, controller.signal);
835
+ if (streamOpts.fallbackManaged) {
836
+ events = observeManagedGatewayFailure(events, error =>
837
+ markManagedGatewayCredentialFailure(
838
+ bootOpts.storage,
839
+ model,
840
+ apiKey,
841
+ error,
842
+ controller.signal,
843
+ route.label,
844
+ peer,
845
+ ),
846
+ );
847
+ }
848
+ events = redactGatewayStream(events);
849
+
850
+ if (!parsed.stream) {
851
+ try {
852
+ if (controller.signal.aborted) return clientClosedResponse(route);
853
+ const message = await events.result();
854
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
855
+ const errorMessage =
856
+ message.errorMessage ??
857
+ (message.stopReason === "aborted" ? "Request was aborted" : "Upstream request failed");
858
+ const safeErrorMessage = cleanReason(errorMessage) ?? "Upstream request failed";
859
+ logger.warn("auth-gateway non-streaming failed", {
860
+ format: route.label,
861
+ reason: message.stopReason,
862
+ error: safeErrorMessage,
863
+ peer,
864
+ });
865
+ if (message.stopReason === "aborted") {
866
+ return route.module.formatError(499, "request_aborted", safeErrorMessage);
867
+ }
868
+ const classified = classifyGatewayError(new Error(safeErrorMessage));
869
+ return route.module.formatError(classified.status, classified.type, classified.message);
870
+ }
871
+ return json(200, route.module.encodeResponse(message, parsed.modelId));
872
+ } catch (error) {
873
+ if (controller.signal.aborted) return clientClosedResponse(route);
874
+ const classified = classifyGatewayError(error);
875
+ logger.warn("auth-gateway non-streaming aborted", {
876
+ format: route.label,
877
+ error: classified.message,
878
+ peer,
879
+ });
880
+ return route.module.formatError(classified.status, classified.type, classified.message);
881
+ }
882
+ }
883
+ if (controller.signal.aborted) return clientClosedResponse(route);
884
+
885
+ const sseStream = route.module.encodeStream(events, parsed.modelId, parsed.options);
886
+ return new Response(sseStream, {
887
+ status: 200,
888
+ headers: {
889
+ "Content-Type": "text/event-stream; charset=utf-8",
890
+ "Cache-Control": "no-cache",
891
+ Connection: "keep-alive",
892
+ // Disable proxy buffering (nginx and ingress controllers honor this).
893
+ // Without it the SSE stream gets held until the buffer flushes, which
894
+ // stalls the long-thinking-budget calls we exist to support.
895
+ "X-Accel-Buffering": "no",
896
+ },
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Pi-native fast path: `POST /v1/pi/stream`. Accepts the canonical pi-ai
902
+ * `Context` directly (no wire-format round-trip) and emits a bandwidth-shrunk
903
+ * event stream matching `pi-agent`'s `streamProxy`. Skips the OpenAI /
904
+ * Anthropic / Responses translation layers — those exist to bridge foreign
905
+ * SDKs (llm-git, anthropic-sdk, openai-sdk), and bridging back to pi-native
906
+ * just to bridge forward again is wasted work.
907
+ *
908
+ * Every other gateway concern (bearer auth, model resolve, credential fetch,
909
+ * abort mirroring, OpenAI code backend temperature/topP strip, prefix-cache key derivation,
910
+ * Anthropic-code OAuth shaping inside `streamSimple`) still applies — only
911
+ * `parseRequest`/`encodeResponse`/`encodeStream` differ from the format-endpoint
912
+ * path.
913
+ */
914
+ async function handlePiNative(
915
+ bootOpts: AuthGatewayBootOptions,
916
+ catalog: AuthGatewayModelCatalog,
917
+ req: Request,
918
+ peer: string,
919
+ ): Promise<Response> {
920
+ const controller = mirrorRequestAbort(req);
921
+ const aborted = (): Response => piNative.formatError(499, "request_aborted", "client closed request");
922
+ if (controller.signal.aborted) return aborted();
923
+
924
+ let body: unknown;
925
+ try {
926
+ body = await req.json();
927
+ } catch (error) {
928
+ if (controller.signal.aborted) return aborted();
929
+ return piNative.formatError(
930
+ 400,
931
+ "invalid_request_error",
932
+ `Invalid JSON body: ${cleanReason(error) ?? "request body could not be parsed"}`,
933
+ );
934
+ }
935
+ if (controller.signal.aborted) return aborted();
936
+
937
+ let parsed: piNative.PiNativeParsedRequest;
938
+ try {
939
+ parsed = piNative.parseRequest(body, req.headers);
940
+ } catch (error) {
941
+ if (controller.signal.aborted) return aborted();
942
+ const message = cleanReason(error) ?? "Request validation failed";
943
+ return piNative.formatError(400, "invalid_request_error", message);
944
+ }
945
+
946
+ const model = resolveScopedModel(bootOpts, catalog, parsed.modelId);
947
+ if (!model) {
948
+ return piNative.formatError(404, "invalid_request_error", `Unknown model: ${parsed.modelId}`);
949
+ }
950
+
951
+ // Build the SimpleStreamOptions actually handed to `streamSimple`. We
952
+ // trust the client's options (already allow-listed by `parseRequest`) and
953
+ // only inject server-controlled fields. The OpenAI code backend temperature/topP strip
954
+ // matches `buildStreamOptions` — OpenAI code backend rejects them with a 400.
955
+ const streamOpts: SimpleStreamOptions = {
956
+ ...parsed.options,
957
+ signal: controller.signal,
958
+ requestMaxRetries: 0,
959
+ streamMaxRetries: 0,
960
+ disableProviderRetries: true,
961
+ };
962
+ if (streamOpts.fallbackManaged) {
963
+ streamOpts.fallbackAttempt = beginAttempt(model.id, "auth-gateway-pi-native");
964
+ } else {
965
+ streamOpts.onAuthError = (provider, oldKey, error) =>
966
+ refreshGatewayApiKeyAfterAuthError(
967
+ bootOpts,
968
+ model,
969
+ provider,
970
+ oldKey,
971
+ error,
972
+ controller.signal,
973
+ "pi-native",
974
+ peer,
975
+ );
976
+ }
977
+ if (model.api === "openai-codex-responses") {
978
+ delete streamOpts.temperature;
979
+ delete streamOpts.topP;
980
+ }
981
+ // Merge gateway-captured passthrough headers under the client's own
982
+ // headers — the client's values win when they collide.
983
+ const captured = captureRequestHeaders(req.headers);
984
+ streamOpts.headers = { ...captured, ...(streamOpts.headers ?? {}) };
985
+ // Cache identity: explicit `sessionId` wins, then derive a stable key
986
+ // from model + system + tools + first message so OpenAI code backend prefix caching
987
+ // engages on the same logical conversation across turns.
988
+ streamOpts.sessionId ??= deriveSessionId(parsed.modelId, parsed.context);
989
+
990
+ logger.info("auth-gateway request", {
991
+ format: "pi-native",
992
+ model: parsed.modelId,
993
+ resolvedProvider: model.provider,
994
+ resolvedModel: model.id,
995
+ stream: parsed.stream,
996
+ peer,
997
+ });
998
+
999
+ let apiKey: string;
1000
+ let credentialLease: GatewayCredentialLease;
1001
+ try {
1002
+ credentialLease = await acquireGatewayApiKey(bootOpts, model, peer, controller.signal);
1003
+ apiKey = credentialLease.apiKey;
1004
+ streamOpts.apiKey = apiKey;
1005
+ } catch (error) {
1006
+ if (controller.signal.aborted) return aborted();
1007
+ if (error instanceof GatewayCredentialError) {
1008
+ return piNative.formatError(error.status, error.type, error.message);
1009
+ }
1010
+ throw error;
1011
+ }
1012
+
1013
+ let events: AssistantMessageEventStream;
1014
+ let releasedAtAdmission = false;
1015
+ const releaseAtAdmission = (): void => {
1016
+ if (releasedAtAdmission) return;
1017
+ releasedAtAdmission = true;
1018
+ credentialLease.release();
1019
+ };
1020
+ streamOpts.onStreamCreated = releaseAtAdmission;
1021
+ try {
1022
+ if (controller.signal.aborted) {
1023
+ credentialLease.release();
1024
+ return aborted();
1025
+ }
1026
+ events = streamSimple(model, parsed.context, streamOpts);
1027
+ } catch (error) {
1028
+ credentialLease.release();
1029
+ if (streamOpts.fallbackManaged) {
1030
+ await markManagedGatewayCredentialFailure(
1031
+ bootOpts.storage,
1032
+ model,
1033
+ apiKey,
1034
+ error,
1035
+ controller.signal,
1036
+ "pi-native",
1037
+ peer,
1038
+ );
1039
+ }
1040
+ const classified = classifyGatewayError(error);
1041
+ logger.warn("auth-gateway streamSimple threw", { format: "pi-native", error: classified.message, peer });
1042
+ return piNative.formatError(classified.status, classified.type, classified.message);
1043
+ }
1044
+ releaseGatewayCredentialLeaseOnAdmission(events, releaseAtAdmission, controller.signal);
1045
+ if (streamOpts.fallbackManaged) {
1046
+ events = observeManagedGatewayFailure(events, error =>
1047
+ markManagedGatewayCredentialFailure(
1048
+ bootOpts.storage,
1049
+ model,
1050
+ apiKey,
1051
+ error,
1052
+ controller.signal,
1053
+ "pi-native",
1054
+ peer,
1055
+ ),
1056
+ );
1057
+ }
1058
+ events = redactGatewayStream(events);
1059
+
1060
+ if (!parsed.stream) {
1061
+ try {
1062
+ if (controller.signal.aborted) return aborted();
1063
+ const message = await events.result();
1064
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
1065
+ const errorMessage =
1066
+ message.errorMessage ??
1067
+ (message.stopReason === "aborted" ? "Request was aborted" : "Upstream request failed");
1068
+ const safeErrorMessage = cleanReason(errorMessage) ?? "Upstream request failed";
1069
+ logger.warn("auth-gateway non-streaming failed", {
1070
+ format: "pi-native",
1071
+ reason: message.stopReason,
1072
+ error: safeErrorMessage,
1073
+ peer,
1074
+ });
1075
+ if (message.stopReason === "aborted") {
1076
+ return piNative.formatError(499, "request_aborted", safeErrorMessage);
1077
+ }
1078
+ const classified = classifyGatewayError(new Error(safeErrorMessage));
1079
+ return piNative.formatError(classified.status, classified.type, classified.message);
1080
+ }
1081
+ return json(200, { message });
1082
+ } catch (error) {
1083
+ if (controller.signal.aborted) return aborted();
1084
+ const classified = classifyGatewayError(error);
1085
+ logger.warn("auth-gateway non-streaming aborted", { format: "pi-native", error: classified.message, peer });
1086
+ return piNative.formatError(classified.status, classified.type, classified.message);
1087
+ }
1088
+ }
1089
+ if (controller.signal.aborted) return aborted();
1090
+
1091
+ const sseStream = piNative.encodeStream(events);
1092
+ return new Response(sseStream, {
1093
+ status: 200,
1094
+ headers: {
1095
+ "Content-Type": "text/event-stream; charset=utf-8",
1096
+ "Cache-Control": "no-cache",
1097
+ Connection: "keep-alive",
1098
+ "X-Accel-Buffering": "no",
1099
+ },
1100
+ });
1101
+ }
1102
+
1103
+ /**
1104
+ * Snapshot of `GET /v1/usage` — `fetchUsageReports` already caches reports at
1105
+ * a 5-minute per-credential TTL (with jitter, plus last-good fallback on
1106
+ * failure) inside `AuthStorage`, so this handler is a thin wrapper that
1107
+ * surfaces the same data to HTTP callers (notably the macOS usage widget).
1108
+ */
1109
+ async function handleUsage(storage: AuthStorage, provider: Provider, signal: AbortSignal): Promise<Response> {
1110
+ const fetchedReports = await storage.fetchUsageReports?.({ provider, signal });
1111
+ if (fetchedReports === null || fetchedReports === undefined) {
1112
+ throw new Error("Usage unavailable.");
1113
+ }
1114
+ const reports = fetchedReports.filter(report => report.provider === provider);
1115
+ // Drop the heavy provider-specific `raw` payload — UI consumers only need
1116
+ // `limits` + `metadata`. Match the broker's `/v1/usage` shape so a single
1117
+ // client struct (Swift widget, llm-git, ...) works against either endpoint.
1118
+ const trimmed = reports.map(({ raw: _raw, ...rest }) => rest);
1119
+ return json(200, { generatedAt: Date.now(), reports: trimmed });
1120
+ }
1121
+
1122
+ function emptyScopedCredentialsResponse(): Response {
1123
+ return json(200, { generatedAt: Date.now(), credentials: [] });
1124
+ }
1125
+
1126
+ /**
1127
+ * Per-credential health probe surfaced on `GET /v1/credentials/check`. Tells
1128
+ * the caller exactly which row in their broker is producing 401s — the
1129
+ * aggregate `/v1/usage` endpoint silently drops failed credentials, which is
1130
+ * the wrong shape when you're diagnosing auth.
1131
+ *
1132
+ * The probe is sequential (one credential at a time) to avoid synchronized
1133
+ * N-account fan-out tripping per-IP rate limits on provider `/usage`
1134
+ * endpoints. For multi-account pools that's the difference between getting
1135
+ * a clean diagnosis and getting a 429 storm.
1136
+ */
1137
+ async function handleCredentialsCheck(
1138
+ storage: AuthStorage,
1139
+ provider: Provider,
1140
+ signal: AbortSignal,
1141
+ ): Promise<Response> {
1142
+ const credentials = (await storage.checkCredentials({ provider, signal }))
1143
+ .filter(row => row.provider === provider)
1144
+ .map(row => ({
1145
+ id: row.id,
1146
+ provider: row.provider,
1147
+ type: row.type,
1148
+ ...(row.remoteRefresh ? { remoteRefresh: true as const } : {}),
1149
+ ok: row.ok,
1150
+ ...(row.reason
1151
+ ? { reason: row.ok === false ? "Credential check failed." : "Credential status unavailable." }
1152
+ : {}),
1153
+ }));
1154
+ return json(200, { generatedAt: Date.now(), credentials });
1155
+ }
1156
+
1157
+ function handleModelsList(catalog: AuthGatewayModelCatalog): Response {
1158
+ const data = catalog.models.map(model => ({
1159
+ id: model.id,
1160
+ object: "model" as const,
1161
+ owned_by: model.provider,
1162
+ api: model.api,
1163
+ }));
1164
+ return json(200, { object: "list", data });
1165
+ }
1166
+
1167
+ export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServerHandle {
1168
+ const provider = opts.providerScope.provider;
1169
+ if (!isSafeProviderScope(provider)) {
1170
+ throw new Error("Auth gateway requires a valid provider scope");
1171
+ }
1172
+ if (!opts.reloadProviderCredentials || !opts.hasProviderCredential || !opts.validateProviderCredential) {
1173
+ throw new Error("Auth gateway requires live provider authority callbacks");
1174
+ }
1175
+ const bind = parseBind(opts.bind ?? DEFAULT_AUTH_GATEWAY_BIND);
1176
+ if (!hasProviderCredential(opts)) {
1177
+ throw new Error(`Auth gateway scope ${provider} has no enabled broker credential`);
1178
+ }
1179
+ const catalog = createAuthGatewayModelCatalog(provider, opts.listModels());
1180
+ if (catalog.models.length === 0) {
1181
+ throw new Error(`Auth gateway scope ${provider} has no source-backed models`);
1182
+ }
1183
+ const tokens = new Set<string>(opts.bearerTokens);
1184
+ assertAuthenticatedOrLoopback(bind, tokens.size, "auth-gateway");
1185
+ const version = opts.version;
1186
+
1187
+ const server = Bun.serve({
1188
+ hostname: bind.hostname,
1189
+ port: bind.port,
1190
+ fetch: async (req): Promise<Response> => {
1191
+ const url = new URL(req.url);
1192
+ const pathname = url.pathname;
1193
+ const peer = resolvePeer(req);
1194
+ if (isNoAuthBrowserOriginRequest(req, tokens)) {
1195
+ logger.info("auth-gateway no-auth browser-origin request rejected", {
1196
+ method: req.method,
1197
+ path: pathname,
1198
+ peer,
1199
+ origin: req.headers.get("origin"),
1200
+ });
1201
+ return json(403, { error: "no-auth rejects requests carrying Origin" });
1202
+ }
1203
+ // CORS preflight is always answered without auth — browsers send
1204
+ // preflights pre-authentication and a 401 here breaks the actual
1205
+ // request before the bearer is ever attached.
1206
+ if (req.method === "OPTIONS") {
1207
+ return new Response(null, { status: 204, headers: corsHeaders(req) });
1208
+ }
1209
+ try {
1210
+ if (req.method === "GET" && pathname === "/healthz") {
1211
+ return withCors(json(200, { ok: true, version }), req);
1212
+ }
1213
+ if (!isAuthorized(req, tokens)) {
1214
+ logger.info("auth-gateway request unauthorized", { method: req.method, path: pathname, peer });
1215
+ return withCors(json(401, { error: "unauthorized" }), req);
1216
+ }
1217
+
1218
+ // Aggregated usage — backed by AuthStorage's 5-min per-credential cache.
1219
+ // Same shape as the broker's `/v1/usage`, so widget/llm-git speak to either with the
1220
+ // same client struct.
1221
+ if (req.method === "GET" && pathname === "/v1/usage") {
1222
+ const scopeAvailability = await providerScopeAvailability(opts, req.signal);
1223
+ if (scopeAvailability === "reload_failed") {
1224
+ return withCors(
1225
+ json(503, { error: { code: "broker_unavailable", message: "Auth broker unavailable." } }),
1226
+ req,
1227
+ );
1228
+ }
1229
+ if (scopeAvailability === "absent")
1230
+ return withCors(json(200, { generatedAt: Date.now(), reports: [] }), req);
1231
+ try {
1232
+ return withCors(await handleUsage(opts.storage, opts.providerScope.provider, req.signal), req);
1233
+ } catch (error) {
1234
+ logger.warn("auth-gateway scoped usage unavailable", {
1235
+ error: cleanReason(error) ?? "Usage unavailable.",
1236
+ });
1237
+ return withCors(
1238
+ json(503, { error: { code: "usage_unavailable", message: "Usage unavailable." } }),
1239
+ req,
1240
+ );
1241
+ }
1242
+ }
1243
+
1244
+ // Per-credential auth probe — diagnoses which row in a multi-account
1245
+ // pool is producing 401s. Aggregated `/v1/usage` silently drops failed
1246
+ // credentials, so we need a separate endpoint that captures errors.
1247
+ if (req.method === "GET" && pathname === "/v1/credentials/check") {
1248
+ const scopeAvailability = await providerScopeAvailability(opts, req.signal);
1249
+ if (scopeAvailability === "reload_failed") {
1250
+ return withCors(
1251
+ json(503, { error: { code: "broker_unavailable", message: "Auth broker unavailable." } }),
1252
+ req,
1253
+ );
1254
+ }
1255
+ if (scopeAvailability === "absent") return withCors(emptyScopedCredentialsResponse(), req);
1256
+ return withCors(
1257
+ await handleCredentialsCheck(opts.storage, opts.providerScope.provider, req.signal),
1258
+ req,
1259
+ );
1260
+ }
1261
+
1262
+ // Provider-format dispatch.
1263
+ const formatRoute = FORMAT_ROUTES[pathname];
1264
+ if (formatRoute && req.method === "POST") {
1265
+ return withCors(await handleFormatEndpoint(formatRoute, opts, catalog, req, peer), req);
1266
+ }
1267
+
1268
+ // Pi-native fast path. Same auth + provider plumbing as the
1269
+ // foreign-wire routes, just without the wire-format translation.
1270
+ if (req.method === "POST" && pathname === "/v1/pi/stream") {
1271
+ return withCors(await handlePiNative(opts, catalog, req, peer), req);
1272
+ }
1273
+
1274
+ // Model catalog.
1275
+ if (req.method === "GET" && pathname === "/v1/models") {
1276
+ return withCors(handleModelsList(catalog), req);
1277
+ }
1278
+
1279
+ // Route-table miss: no format module to defer to, so we emit a
1280
+ // plain JSON 404 rather than guessing at a protocol-specific envelope.
1281
+ return withCors(json(404, { error: `No route: ${req.method} ${pathname}` }), req);
1282
+ } catch (error) {
1283
+ logger.error("auth-gateway handler crashed", {
1284
+ method: req.method,
1285
+ path: pathname,
1286
+ peer,
1287
+ error: cleanReason(error) ?? "internal error",
1288
+ });
1289
+ return withCors(json(500, { error: "internal error" }), req);
1290
+ }
1291
+ },
1292
+ // Max-out Bun's idle timeout. Long thinking-budget calls can sit idle
1293
+ // for minutes before the first token arrives; the default kills them.
1294
+ idleTimeout: 255,
1295
+ });
1296
+
1297
+ const boundHost = server.hostname ?? bind.hostname;
1298
+ const boundPort = server.port ?? bind.port;
1299
+ return {
1300
+ url: `http://${boundHost}:${boundPort}`,
1301
+ port: boundPort,
1302
+ hostname: boundHost,
1303
+ close: async () => {
1304
+ server.stop(true);
1305
+ },
1306
+ };
1307
+ }
1308
+
1309
+ export function isSafeProviderScope(provider: unknown): provider is string {
1310
+ return (
1311
+ typeof provider === "string" &&
1312
+ provider === provider.trim() &&
1313
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(provider)
1314
+ );
1315
+ }