@aryee337/aery-ai 0.2.28 → 0.2.29

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 (417) hide show
  1. package/CHANGELOG.md +2914 -0
  2. package/README.md +614 -813
  3. package/package.json +140 -105
  4. package/src/api-registry.ts +96 -0
  5. package/src/auth-broker/client.ts +358 -0
  6. package/src/auth-broker/index.ts +5 -0
  7. package/src/auth-broker/refresher.ts +117 -0
  8. package/src/auth-broker/remote-store.ts +623 -0
  9. package/src/auth-broker/server.ts +644 -0
  10. package/src/auth-broker/types.ts +127 -0
  11. package/src/auth-broker/wire-schemas.ts +200 -0
  12. package/src/auth-gateway/http.ts +194 -0
  13. package/src/auth-gateway/index.ts +3 -0
  14. package/src/auth-gateway/server.ts +818 -0
  15. package/src/auth-gateway/types.ts +143 -0
  16. package/src/auth-storage.ts +4422 -0
  17. package/src/index.ts +54 -0
  18. package/src/model-cache.ts +129 -0
  19. package/src/model-manager.ts +469 -0
  20. package/src/model-thinking.ts +782 -0
  21. package/src/models.json +83530 -0
  22. package/src/models.json.d.ts +9 -0
  23. package/src/models.ts +56 -0
  24. package/src/prompts/turn-aborted-guidance.md +4 -0
  25. package/src/provider-details.ts +90 -0
  26. package/src/provider-models/bundled-references.ts +38 -0
  27. package/src/provider-models/descriptors.ts +355 -0
  28. package/src/provider-models/google.ts +88 -0
  29. package/src/provider-models/index.ts +5 -0
  30. package/src/provider-models/ollama.ts +153 -0
  31. package/src/provider-models/openai-compat.ts +2817 -0
  32. package/src/provider-models/special.ts +67 -0
  33. package/src/providers/aery-native-client.ts +228 -0
  34. package/src/providers/aery-native-server.ts +212 -0
  35. package/src/providers/amazon-bedrock.ts +873 -0
  36. package/src/providers/anthropic-client.ts +318 -0
  37. package/src/providers/anthropic-messages-server-schema.ts +243 -0
  38. package/src/providers/anthropic-messages-server.ts +683 -0
  39. package/src/providers/anthropic-wire.ts +268 -0
  40. package/src/providers/anthropic.ts +3094 -0
  41. package/src/providers/aws-credentials.ts +501 -0
  42. package/src/providers/aws-eventstream.ts +185 -0
  43. package/src/providers/aws-sigv4.ts +218 -0
  44. package/src/providers/azure-openai-responses.ts +361 -0
  45. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  46. package/src/providers/cursor/proto/agent.proto +3526 -0
  47. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  48. package/src/providers/cursor/proto/buf.yaml +17 -0
  49. package/src/providers/cursor.ts +2621 -0
  50. package/src/providers/error-message.ts +21 -0
  51. package/src/providers/github-copilot-headers.ts +140 -0
  52. package/src/providers/gitlab-duo.ts +372 -0
  53. package/src/providers/google-auth.ts +252 -0
  54. package/src/providers/google-gemini-cli.ts +809 -0
  55. package/src/providers/google-gemini-headers.ts +41 -0
  56. package/src/providers/google-shared.ts +917 -0
  57. package/src/providers/google-types.ts +167 -0
  58. package/src/providers/google-vertex.ts +91 -0
  59. package/src/providers/google.ts +41 -0
  60. package/src/providers/grammar.ts +70 -0
  61. package/src/providers/kimi.ts +52 -0
  62. package/src/providers/mock.ts +496 -0
  63. package/src/providers/ollama.ts +644 -0
  64. package/src/providers/openai-anthropic-shim.ts +138 -0
  65. package/src/providers/openai-chat-server-schema.ts +252 -0
  66. package/src/providers/openai-chat-server.ts +647 -0
  67. package/src/providers/openai-codex/constants.ts +43 -0
  68. package/src/providers/openai-codex/request-transformer.ts +161 -0
  69. package/src/providers/openai-codex/response-handler.ts +81 -0
  70. package/src/providers/openai-codex-responses.ts +3018 -0
  71. package/src/providers/openai-completions-compat.ts +300 -0
  72. package/src/providers/openai-completions.ts +1979 -0
  73. package/src/providers/openai-responses-server-schema.ts +290 -0
  74. package/src/providers/openai-responses-server.ts +1183 -0
  75. package/src/providers/openai-responses-shared.ts +873 -0
  76. package/src/providers/openai-responses.ts +679 -0
  77. package/src/providers/register-builtins.ts +436 -0
  78. package/src/providers/synthetic.ts +50 -0
  79. package/src/providers/transform-messages.ts +382 -0
  80. package/src/providers/vision-guard.ts +31 -0
  81. package/src/providers/xai-responses.ts +82 -0
  82. package/src/rate-limit-utils.ts +84 -0
  83. package/src/stream.ts +1065 -0
  84. package/src/types.ts +944 -0
  85. package/src/usage/claude.ts +482 -0
  86. package/src/usage/gemini.ts +250 -0
  87. package/src/usage/github-copilot.ts +421 -0
  88. package/src/usage/google-antigravity.ts +201 -0
  89. package/src/usage/kimi.ts +271 -0
  90. package/src/usage/minimax-code.ts +31 -0
  91. package/src/usage/openai-codex.ts +503 -0
  92. package/src/usage/shared.ts +10 -0
  93. package/src/usage/zai.ts +247 -0
  94. package/src/usage.ts +185 -0
  95. package/src/utils/abort.ts +51 -0
  96. package/src/utils/abortable-iterator.ts +69 -0
  97. package/src/utils/anthropic-auth.ts +93 -0
  98. package/src/utils/discovery/antigravity.ts +261 -0
  99. package/src/utils/discovery/codex.ts +371 -0
  100. package/src/utils/discovery/cursor.ts +306 -0
  101. package/src/utils/discovery/gemini.ts +248 -0
  102. package/src/utils/discovery/index.ts +4 -0
  103. package/src/utils/discovery/openai-compatible.ts +224 -0
  104. package/src/utils/event-stream.ts +142 -0
  105. package/src/utils/fireworks-model-id.ts +30 -0
  106. package/src/utils/foundry.ts +8 -0
  107. package/src/utils/http-inspector.ts +176 -0
  108. package/src/utils/idle-iterator.ts +267 -0
  109. package/src/utils/json-parse.ts +182 -0
  110. package/src/utils/oauth/__tests__/xai-oauth.test.ts +107 -0
  111. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  112. package/src/utils/oauth/anthropic.ts +273 -0
  113. package/src/utils/oauth/api-key-login.ts +87 -0
  114. package/src/utils/oauth/api-key-validation.ts +92 -0
  115. package/src/utils/oauth/callback-server.ts +276 -0
  116. package/src/utils/oauth/cerebras.ts +16 -0
  117. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  118. package/src/utils/oauth/cursor.ts +157 -0
  119. package/src/utils/oauth/deepseek.ts +53 -0
  120. package/src/utils/oauth/firepass.ts +24 -0
  121. package/src/utils/oauth/fireworks.ts +15 -0
  122. package/src/utils/oauth/github-copilot.ts +362 -0
  123. package/src/utils/oauth/gitlab-duo.ts +123 -0
  124. package/src/utils/oauth/google-antigravity.ts +200 -0
  125. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  126. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  127. package/src/utils/oauth/huggingface.ts +62 -0
  128. package/src/utils/oauth/index.ts +484 -0
  129. package/src/utils/oauth/kagi.ts +47 -0
  130. package/src/utils/oauth/kilo.ts +87 -0
  131. package/src/utils/oauth/kimi.ts +254 -0
  132. package/src/utils/oauth/litellm.ts +47 -0
  133. package/src/utils/oauth/lm-studio.ts +38 -0
  134. package/src/utils/oauth/minimax-code.ts +78 -0
  135. package/src/utils/oauth/moonshot.ts +23 -0
  136. package/src/utils/oauth/nanogpt.ts +15 -0
  137. package/src/utils/oauth/nvidia.ts +70 -0
  138. package/src/utils/oauth/oauth.html +203 -0
  139. package/src/utils/oauth/ollama-cloud.ts +28 -0
  140. package/src/utils/oauth/ollama.ts +47 -0
  141. package/src/utils/oauth/openai-codex.ts +299 -0
  142. package/src/utils/oauth/opencode.ts +49 -0
  143. package/src/utils/oauth/openrouter.ts +20 -0
  144. package/src/utils/oauth/parallel.ts +46 -0
  145. package/src/utils/oauth/perplexity.ts +206 -0
  146. package/src/utils/oauth/pkce.ts +18 -0
  147. package/src/utils/oauth/qianfan.ts +58 -0
  148. package/src/utils/oauth/qwen-portal.ts +60 -0
  149. package/src/utils/oauth/synthetic.ts +15 -0
  150. package/src/utils/oauth/tavily.ts +46 -0
  151. package/src/utils/oauth/together.ts +16 -0
  152. package/src/utils/oauth/types.ts +99 -0
  153. package/src/utils/oauth/venice.ts +59 -0
  154. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  155. package/src/utils/oauth/vllm.ts +40 -0
  156. package/src/utils/oauth/wafer.ts +50 -0
  157. package/src/utils/oauth/xai-oauth.ts +342 -0
  158. package/src/utils/oauth/xiaomi.ts +139 -0
  159. package/src/utils/oauth/zai.ts +60 -0
  160. package/src/utils/oauth/zenmux.ts +15 -0
  161. package/src/utils/oauth/zhipu.ts +60 -0
  162. package/src/utils/overflow.ts +137 -0
  163. package/src/utils/parse-bind.ts +54 -0
  164. package/src/utils/provider-response.ts +30 -0
  165. package/src/utils/request-debug.ts +336 -0
  166. package/src/utils/retry-after.ts +110 -0
  167. package/src/utils/retry.ts +54 -0
  168. package/src/utils/schema/CONSTRAINTS.md +164 -0
  169. package/src/utils/schema/adapt.ts +36 -0
  170. package/src/utils/schema/compatibility.ts +435 -0
  171. package/src/utils/schema/dereference.ts +98 -0
  172. package/src/utils/schema/draft.ts +341 -0
  173. package/src/utils/schema/equality.ts +97 -0
  174. package/src/utils/schema/fields.ts +191 -0
  175. package/src/utils/schema/index.ts +13 -0
  176. package/src/utils/schema/json-schema-validator.ts +577 -0
  177. package/src/utils/schema/meta-validator.ts +167 -0
  178. package/src/utils/schema/normalize.ts +1588 -0
  179. package/src/utils/schema/spill.ts +43 -0
  180. package/src/utils/schema/stamps.ts +97 -0
  181. package/src/utils/schema/types.ts +10 -0
  182. package/src/utils/schema/wire.ts +293 -0
  183. package/src/utils/schema/zod-decontaminate.ts +331 -0
  184. package/src/utils/sdk-stream-timeout.ts +43 -0
  185. package/src/utils/sse-debug.ts +289 -0
  186. package/src/utils/stream-markup-healing.ts +612 -0
  187. package/src/utils/tool-choice.ts +99 -0
  188. package/src/utils/validation.ts +1024 -0
  189. package/src/utils.ts +166 -0
  190. package/dist/api-registry.d.ts +0 -20
  191. package/dist/api-registry.d.ts.map +0 -1
  192. package/dist/api-registry.js +0 -44
  193. package/dist/api-registry.js.map +0 -1
  194. package/dist/bedrock-provider.d.ts +0 -5
  195. package/dist/bedrock-provider.d.ts.map +0 -1
  196. package/dist/bedrock-provider.js +0 -6
  197. package/dist/bedrock-provider.js.map +0 -1
  198. package/dist/cli.d.ts +0 -3
  199. package/dist/cli.d.ts.map +0 -1
  200. package/dist/cli.js +0 -130
  201. package/dist/cli.js.map +0 -1
  202. package/dist/env-api-keys.d.ts +0 -18
  203. package/dist/env-api-keys.d.ts.map +0 -1
  204. package/dist/env-api-keys.js +0 -178
  205. package/dist/env-api-keys.js.map +0 -1
  206. package/dist/image-models.d.ts +0 -10
  207. package/dist/image-models.d.ts.map +0 -1
  208. package/dist/image-models.generated.d.ts +0 -440
  209. package/dist/image-models.generated.d.ts.map +0 -1
  210. package/dist/image-models.generated.js +0 -442
  211. package/dist/image-models.generated.js.map +0 -1
  212. package/dist/image-models.js +0 -23
  213. package/dist/image-models.js.map +0 -1
  214. package/dist/images-api-registry.d.ts +0 -14
  215. package/dist/images-api-registry.d.ts.map +0 -1
  216. package/dist/images-api-registry.js +0 -22
  217. package/dist/images-api-registry.js.map +0 -1
  218. package/dist/images.d.ts +0 -4
  219. package/dist/images.d.ts.map +0 -1
  220. package/dist/images.js +0 -14
  221. package/dist/images.js.map +0 -1
  222. package/dist/index.d.ts +0 -32
  223. package/dist/index.d.ts.map +0 -1
  224. package/dist/index.js +0 -20
  225. package/dist/index.js.map +0 -1
  226. package/dist/models.d.ts +0 -18
  227. package/dist/models.d.ts.map +0 -1
  228. package/dist/models.generated.d.ts +0 -17707
  229. package/dist/models.generated.d.ts.map +0 -1
  230. package/dist/models.generated.js +0 -16561
  231. package/dist/models.generated.js.map +0 -1
  232. package/dist/models.js +0 -71
  233. package/dist/models.js.map +0 -1
  234. package/dist/oauth.d.ts +0 -2
  235. package/dist/oauth.d.ts.map +0 -1
  236. package/dist/oauth.js +0 -2
  237. package/dist/oauth.js.map +0 -1
  238. package/dist/providers/aery-error-formatting.d.ts +0 -13
  239. package/dist/providers/aery-error-formatting.d.ts.map +0 -1
  240. package/dist/providers/aery-error-formatting.js +0 -112
  241. package/dist/providers/aery-error-formatting.js.map +0 -1
  242. package/dist/providers/amazon-bedrock.d.ts +0 -38
  243. package/dist/providers/amazon-bedrock.d.ts.map +0 -1
  244. package/dist/providers/amazon-bedrock.js +0 -763
  245. package/dist/providers/amazon-bedrock.js.map +0 -1
  246. package/dist/providers/anthropic.d.ts +0 -71
  247. package/dist/providers/anthropic.d.ts.map +0 -1
  248. package/dist/providers/anthropic.js +0 -949
  249. package/dist/providers/anthropic.js.map +0 -1
  250. package/dist/providers/azure-openai-responses.d.ts +0 -15
  251. package/dist/providers/azure-openai-responses.d.ts.map +0 -1
  252. package/dist/providers/azure-openai-responses.js +0 -225
  253. package/dist/providers/azure-openai-responses.js.map +0 -1
  254. package/dist/providers/cloudflare.d.ts +0 -13
  255. package/dist/providers/cloudflare.d.ts.map +0 -1
  256. package/dist/providers/cloudflare.js +0 -26
  257. package/dist/providers/cloudflare.js.map +0 -1
  258. package/dist/providers/faux.d.ts +0 -56
  259. package/dist/providers/faux.d.ts.map +0 -1
  260. package/dist/providers/faux.js +0 -368
  261. package/dist/providers/faux.js.map +0 -1
  262. package/dist/providers/github-copilot-headers.d.ts +0 -8
  263. package/dist/providers/github-copilot-headers.d.ts.map +0 -1
  264. package/dist/providers/github-copilot-headers.js +0 -29
  265. package/dist/providers/github-copilot-headers.js.map +0 -1
  266. package/dist/providers/google-gemini-cli.d.ts +0 -74
  267. package/dist/providers/google-gemini-cli.d.ts.map +0 -1
  268. package/dist/providers/google-gemini-cli.js +0 -779
  269. package/dist/providers/google-gemini-cli.js.map +0 -1
  270. package/dist/providers/google-shared.d.ts +0 -70
  271. package/dist/providers/google-shared.d.ts.map +0 -1
  272. package/dist/providers/google-shared.js +0 -329
  273. package/dist/providers/google-shared.js.map +0 -1
  274. package/dist/providers/google-vertex.d.ts +0 -15
  275. package/dist/providers/google-vertex.d.ts.map +0 -1
  276. package/dist/providers/google-vertex.js +0 -442
  277. package/dist/providers/google-vertex.js.map +0 -1
  278. package/dist/providers/google.d.ts +0 -13
  279. package/dist/providers/google.d.ts.map +0 -1
  280. package/dist/providers/google.js +0 -400
  281. package/dist/providers/google.js.map +0 -1
  282. package/dist/providers/images/openrouter.d.ts +0 -3
  283. package/dist/providers/images/openrouter.d.ts.map +0 -1
  284. package/dist/providers/images/openrouter.js +0 -129
  285. package/dist/providers/images/openrouter.js.map +0 -1
  286. package/dist/providers/images/register-builtins.d.ts +0 -4
  287. package/dist/providers/images/register-builtins.d.ts.map +0 -1
  288. package/dist/providers/images/register-builtins.js +0 -34
  289. package/dist/providers/images/register-builtins.js.map +0 -1
  290. package/dist/providers/mistral.d.ts +0 -25
  291. package/dist/providers/mistral.d.ts.map +0 -1
  292. package/dist/providers/mistral.js +0 -535
  293. package/dist/providers/mistral.js.map +0 -1
  294. package/dist/providers/openai-codex-responses.d.ts +0 -30
  295. package/dist/providers/openai-codex-responses.d.ts.map +0 -1
  296. package/dist/providers/openai-codex-responses.js +0 -1090
  297. package/dist/providers/openai-codex-responses.js.map +0 -1
  298. package/dist/providers/openai-completions.d.ts +0 -19
  299. package/dist/providers/openai-completions.d.ts.map +0 -1
  300. package/dist/providers/openai-completions.js +0 -950
  301. package/dist/providers/openai-completions.js.map +0 -1
  302. package/dist/providers/openai-prompt-cache.d.ts +0 -3
  303. package/dist/providers/openai-prompt-cache.d.ts.map +0 -1
  304. package/dist/providers/openai-prompt-cache.js +0 -10
  305. package/dist/providers/openai-prompt-cache.js.map +0 -1
  306. package/dist/providers/openai-responses-shared.d.ts +0 -18
  307. package/dist/providers/openai-responses-shared.d.ts.map +0 -1
  308. package/dist/providers/openai-responses-shared.js +0 -492
  309. package/dist/providers/openai-responses-shared.js.map +0 -1
  310. package/dist/providers/openai-responses.d.ts +0 -13
  311. package/dist/providers/openai-responses.d.ts.map +0 -1
  312. package/dist/providers/openai-responses.js +0 -237
  313. package/dist/providers/openai-responses.js.map +0 -1
  314. package/dist/providers/register-builtins.d.ts +0 -38
  315. package/dist/providers/register-builtins.d.ts.map +0 -1
  316. package/dist/providers/register-builtins.js +0 -278
  317. package/dist/providers/register-builtins.js.map +0 -1
  318. package/dist/providers/simple-options.d.ts +0 -8
  319. package/dist/providers/simple-options.d.ts.map +0 -1
  320. package/dist/providers/simple-options.js +0 -41
  321. package/dist/providers/simple-options.js.map +0 -1
  322. package/dist/providers/transform-messages.d.ts +0 -8
  323. package/dist/providers/transform-messages.d.ts.map +0 -1
  324. package/dist/providers/transform-messages.js +0 -184
  325. package/dist/providers/transform-messages.js.map +0 -1
  326. package/dist/session-resources.d.ts +0 -4
  327. package/dist/session-resources.d.ts.map +0 -1
  328. package/dist/session-resources.js +0 -22
  329. package/dist/session-resources.js.map +0 -1
  330. package/dist/stream.d.ts +0 -8
  331. package/dist/stream.d.ts.map +0 -1
  332. package/dist/stream.js +0 -27
  333. package/dist/stream.js.map +0 -1
  334. package/dist/types.d.ts +0 -498
  335. package/dist/types.d.ts.map +0 -1
  336. package/dist/types.js +0 -2
  337. package/dist/types.js.map +0 -1
  338. package/dist/utils/diagnostics.d.ts +0 -19
  339. package/dist/utils/diagnostics.d.ts.map +0 -1
  340. package/dist/utils/diagnostics.js +0 -25
  341. package/dist/utils/diagnostics.js.map +0 -1
  342. package/dist/utils/event-stream.d.ts +0 -21
  343. package/dist/utils/event-stream.d.ts.map +0 -1
  344. package/dist/utils/event-stream.js +0 -81
  345. package/dist/utils/event-stream.js.map +0 -1
  346. package/dist/utils/hash.d.ts +0 -3
  347. package/dist/utils/hash.d.ts.map +0 -1
  348. package/dist/utils/hash.js +0 -14
  349. package/dist/utils/hash.js.map +0 -1
  350. package/dist/utils/headers.d.ts +0 -2
  351. package/dist/utils/headers.d.ts.map +0 -1
  352. package/dist/utils/headers.js +0 -8
  353. package/dist/utils/headers.js.map +0 -1
  354. package/dist/utils/json-parse.d.ts +0 -16
  355. package/dist/utils/json-parse.d.ts.map +0 -1
  356. package/dist/utils/json-parse.js +0 -113
  357. package/dist/utils/json-parse.js.map +0 -1
  358. package/dist/utils/node-http-proxy.d.ts +0 -10
  359. package/dist/utils/node-http-proxy.d.ts.map +0 -1
  360. package/dist/utils/node-http-proxy.js +0 -97
  361. package/dist/utils/node-http-proxy.js.map +0 -1
  362. package/dist/utils/oauth/anthropic.d.ts +0 -25
  363. package/dist/utils/oauth/anthropic.d.ts.map +0 -1
  364. package/dist/utils/oauth/anthropic.js +0 -335
  365. package/dist/utils/oauth/anthropic.js.map +0 -1
  366. package/dist/utils/oauth/device-code.d.ts +0 -19
  367. package/dist/utils/oauth/device-code.d.ts.map +0 -1
  368. package/dist/utils/oauth/device-code.js +0 -55
  369. package/dist/utils/oauth/device-code.js.map +0 -1
  370. package/dist/utils/oauth/github-copilot.d.ts +0 -30
  371. package/dist/utils/oauth/github-copilot.d.ts.map +0 -1
  372. package/dist/utils/oauth/github-copilot.js +0 -268
  373. package/dist/utils/oauth/github-copilot.js.map +0 -1
  374. package/dist/utils/oauth/google-antigravity.d.ts +0 -26
  375. package/dist/utils/oauth/google-antigravity.d.ts.map +0 -1
  376. package/dist/utils/oauth/google-antigravity.js +0 -377
  377. package/dist/utils/oauth/google-antigravity.js.map +0 -1
  378. package/dist/utils/oauth/google-gemini-cli.d.ts +0 -26
  379. package/dist/utils/oauth/google-gemini-cli.d.ts.map +0 -1
  380. package/dist/utils/oauth/google-gemini-cli.js +0 -482
  381. package/dist/utils/oauth/google-gemini-cli.js.map +0 -1
  382. package/dist/utils/oauth/index.d.ts +0 -63
  383. package/dist/utils/oauth/index.d.ts.map +0 -1
  384. package/dist/utils/oauth/index.js +0 -131
  385. package/dist/utils/oauth/index.js.map +0 -1
  386. package/dist/utils/oauth/oauth-page.d.ts +0 -3
  387. package/dist/utils/oauth/oauth-page.d.ts.map +0 -1
  388. package/dist/utils/oauth/oauth-page.js +0 -105
  389. package/dist/utils/oauth/oauth-page.js.map +0 -1
  390. package/dist/utils/oauth/openai-codex.d.ts +0 -34
  391. package/dist/utils/oauth/openai-codex.d.ts.map +0 -1
  392. package/dist/utils/oauth/openai-codex.js +0 -385
  393. package/dist/utils/oauth/openai-codex.js.map +0 -1
  394. package/dist/utils/oauth/pkce.d.ts +0 -13
  395. package/dist/utils/oauth/pkce.d.ts.map +0 -1
  396. package/dist/utils/oauth/pkce.js +0 -31
  397. package/dist/utils/oauth/pkce.js.map +0 -1
  398. package/dist/utils/oauth/types.d.ts +0 -64
  399. package/dist/utils/oauth/types.d.ts.map +0 -1
  400. package/dist/utils/oauth/types.js +0 -2
  401. package/dist/utils/oauth/types.js.map +0 -1
  402. package/dist/utils/overflow.d.ts +0 -56
  403. package/dist/utils/overflow.d.ts.map +0 -1
  404. package/dist/utils/overflow.js +0 -151
  405. package/dist/utils/overflow.js.map +0 -1
  406. package/dist/utils/sanitize-unicode.d.ts +0 -22
  407. package/dist/utils/sanitize-unicode.d.ts.map +0 -1
  408. package/dist/utils/sanitize-unicode.js +0 -26
  409. package/dist/utils/sanitize-unicode.js.map +0 -1
  410. package/dist/utils/typebox-helpers.d.ts +0 -17
  411. package/dist/utils/typebox-helpers.d.ts.map +0 -1
  412. package/dist/utils/typebox-helpers.js +0 -21
  413. package/dist/utils/typebox-helpers.js.map +0 -1
  414. package/dist/utils/validation.d.ts +0 -18
  415. package/dist/utils/validation.d.ts.map +0 -1
  416. package/dist/utils/validation.js +0 -281
  417. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,267 @@
1
+ import { $env } from "@aryee337/aery-utils";
2
+
3
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
4
+ const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
5
+
6
+ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
7
+ if (value === undefined) return fallback;
8
+ const parsed = Number(value);
9
+ if (!Number.isFinite(parsed)) return fallback;
10
+ if (parsed <= 0) return undefined;
11
+ return Math.trunc(parsed);
12
+ }
13
+
14
+ /**
15
+ * Returns the idle timeout used for provider streaming transports.
16
+ *
17
+ * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is accepted as a backward-compatible alias.
18
+ * Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
19
+ *
20
+ * Providers that legitimately stream much slower than the global default can pass
21
+ * `fallbackMs` to widen the floor used when neither env var nor caller option is set.
22
+ * Caller options still take precedence; env overrides still trump the fallback.
23
+ */
24
+ export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
25
+ return normalizeIdleTimeoutMs($env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
26
+ }
27
+
28
+ /**
29
+ * Returns the idle timeout used for OpenAI-family streaming transports.
30
+ *
31
+ * `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` takes precedence over the generic
32
+ * `PI_STREAM_IDLE_TIMEOUT_MS` because some deployments tune OpenAI-compatible
33
+ * backends separately from Anthropic/Gemini-style transports.
34
+ *
35
+ * Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
36
+ */
37
+ export function getOpenAIStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
38
+ return normalizeIdleTimeoutMs($env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
39
+ }
40
+
41
+ /**
42
+ * Returns the timeout used while waiting for the first stream event.
43
+ * The first token can legitimately take longer than later inter-event gaps,
44
+ * so the default never undershoots the steady-state idle timeout.
45
+ *
46
+ * Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog.
47
+ *
48
+ * Providers whose first response can legitimately take longer (heavy reasoning,
49
+ * slow cold-start proxies) can pass `fallbackMs` to widen the floor used when
50
+ * neither env var nor caller option is set. Caller options still take precedence;
51
+ * env overrides still trump the fallback.
52
+ */
53
+ export function getStreamFirstEventTimeoutMs(
54
+ idleTimeoutMs?: number,
55
+ fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
56
+ ): number | undefined {
57
+ const fallback = idleTimeoutMs === undefined ? fallbackMs : Math.max(fallbackMs, idleTimeoutMs);
58
+ return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback);
59
+ }
60
+
61
+ /**
62
+ * Returns the first-event timeout used for OpenAI-family streaming transports.
63
+ *
64
+ * Precedence: explicit `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` (including a
65
+ * `"0"` disable) wins outright. Otherwise the resolved idle (caller-supplied
66
+ * `idleTimeoutMs` — which itself already encompasses per-call
67
+ * `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` resolved
68
+ * upstream) floors the first-event budget so slow local OpenAI-compatible
69
+ * servers are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS`
70
+ * or the global default during prompt processing.
71
+ *
72
+ * Returns `undefined` when an explicit env knob disables the watchdog.
73
+ */
74
+ export function getOpenAIStreamFirstEventTimeoutMs(
75
+ idleTimeoutMs?: number,
76
+ fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
77
+ ): number | undefined {
78
+ const openAIFirstEventRaw = $env.PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS;
79
+ if (openAIFirstEventRaw !== undefined) {
80
+ return normalizeIdleTimeoutMs(openAIFirstEventRaw, fallbackMs);
81
+ }
82
+ const base = normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallbackMs);
83
+ if (base === undefined) return undefined;
84
+ if (idleTimeoutMs === undefined || idleTimeoutMs <= 0) return base;
85
+ return Math.max(base, idleTimeoutMs);
86
+ }
87
+
88
+ export interface IdleTimeoutIteratorOptions {
89
+ idleTimeoutMs?: number;
90
+ firstItemTimeoutMs?: number;
91
+ errorMessage: string;
92
+ firstItemErrorMessage?: string;
93
+ onIdle?: () => void;
94
+ onFirstItemTimeout?: () => void;
95
+ /**
96
+ * Optional semantic-progress predicate. Non-progress items are still yielded,
97
+ * but they do not reset the idle deadline. This prevents provider
98
+ * keepalive/no-op events from keeping a stalled tool call alive forever.
99
+ */
100
+ isProgressItem?: (item: unknown) => boolean;
101
+ /**
102
+ * Cancel iteration as soon as this signal aborts. Required for caller-driven
103
+ * cancellation (ESC) when the underlying transport does not surface signal
104
+ * aborts to the iterator (HTTP/2 proxies, native sockets, mocked fetch).
105
+ * Without this, the consumer sleeps on iterator.next() until the idle/first
106
+ * -event watchdog fires — observable as the issue #912 "Working… forever"
107
+ * symptom on the github-copilot provider.
108
+ */
109
+ abortSignal?: AbortSignal;
110
+ }
111
+
112
+ /**
113
+ * Yields items from an async iterable while enforcing a maximum idle gap between items.
114
+ *
115
+ * The first item may use a shorter timeout so stuck requests can be aborted and retried
116
+ * before any user-visible content has streamed.
117
+ */
118
+ export async function* iterateWithIdleTimeout<T>(
119
+ iterable: AsyncIterable<T>,
120
+ options: IdleTimeoutIteratorOptions,
121
+ ): AsyncGenerator<T> {
122
+ const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
123
+ const firstItemDeadlineMs =
124
+ firstItemTimeoutMs !== undefined && firstItemTimeoutMs > 0 ? Date.now() + firstItemTimeoutMs : undefined;
125
+ const abortSignal = options.abortSignal;
126
+ const iterator = iterable[Symbol.asyncIterator]();
127
+
128
+ const closeIterator = (): void => {
129
+ const returnPromise = iterator.return?.();
130
+ if (returnPromise) {
131
+ void returnPromise.catch(() => {});
132
+ }
133
+ };
134
+
135
+ if (abortSignal?.aborted) {
136
+ closeIterator();
137
+ throw abortReason(abortSignal);
138
+ }
139
+
140
+ const withRacy = <T>(promise: Promise<T>) =>
141
+ promise.then(
142
+ result => ({ kind: "next" as const, result }),
143
+ error => ({ kind: "error" as const, error }),
144
+ );
145
+
146
+ let awaitingFirstItem = true;
147
+ const markFirstItemReceived = () => {
148
+ awaitingFirstItem = false;
149
+ };
150
+ const isProgressItem = (item: T): boolean => {
151
+ if (!options.isProgressItem) return true;
152
+ try {
153
+ return options.isProgressItem(item);
154
+ } catch {
155
+ return true;
156
+ }
157
+ };
158
+ let lastProgressAt = Date.now();
159
+
160
+ const noTimeoutEnforced =
161
+ (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
162
+ (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
163
+
164
+ while (true) {
165
+ let activeTimeoutMs: number | undefined;
166
+ if (awaitingFirstItem) {
167
+ if (firstItemDeadlineMs !== undefined) {
168
+ activeTimeoutMs = firstItemDeadlineMs - Date.now();
169
+ if (activeTimeoutMs <= 0) {
170
+ options.onFirstItemTimeout?.();
171
+ closeIterator();
172
+ throw new Error(options.firstItemErrorMessage ?? options.errorMessage);
173
+ }
174
+ }
175
+ } else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
176
+ activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
177
+ if (activeTimeoutMs <= 0) {
178
+ options.onIdle?.();
179
+ closeIterator();
180
+ throw new Error(options.errorMessage);
181
+ }
182
+ }
183
+
184
+ const nextResultPromise = withRacy(iterator.next());
185
+
186
+ const racers: Array<
187
+ Promise<
188
+ | { kind: "next"; result: IteratorResult<T> }
189
+ | { kind: "error"; error: unknown }
190
+ | { kind: "timeout" }
191
+ | { kind: "abort" }
192
+ >
193
+ > = [nextResultPromise];
194
+
195
+ let timer: NodeJS.Timeout | undefined;
196
+ let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
197
+ const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs > 0;
198
+ if (enforceTimeout) {
199
+ const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
200
+ resolveTimeout = resolve;
201
+ timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
202
+ racers.push(promise);
203
+ }
204
+
205
+ let abortListener: (() => void) | undefined;
206
+ let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
207
+ if (abortSignal) {
208
+ const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
209
+ resolveAbort = resolve;
210
+ abortListener = () => resolve({ kind: "abort" });
211
+ abortSignal.addEventListener("abort", abortListener, { once: true });
212
+ racers.push(promise);
213
+ }
214
+
215
+ try {
216
+ const outcome = await Promise.race(racers);
217
+ if (outcome.kind === "abort") {
218
+ closeIterator();
219
+ throw abortReason(abortSignal!);
220
+ }
221
+ if (outcome.kind === "timeout") {
222
+ if (!awaitingFirstItem) {
223
+ options.onIdle?.();
224
+ } else {
225
+ options.onFirstItemTimeout?.();
226
+ }
227
+ closeIterator();
228
+ throw new Error(
229
+ !awaitingFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage),
230
+ );
231
+ }
232
+ if (outcome.kind === "error") {
233
+ throw outcome.error;
234
+ }
235
+ if (outcome.result.done) {
236
+ markFirstItemReceived();
237
+ return;
238
+ }
239
+ const item = outcome.result.value;
240
+ // Non-progress items (e.g. provider keepalives, synthetic `start` events that
241
+ // arrive before the model has produced any tokens) MUST NOT flip us out of
242
+ // `awaitingFirstItem`. Otherwise the next iteration switches from the (longer)
243
+ // first-item watchdog to the (shorter) idle watchdog while we're still waiting
244
+ // on the model's first real output.
245
+ if (isProgressItem(item)) {
246
+ markFirstItemReceived();
247
+ lastProgressAt = Date.now();
248
+ }
249
+ yield item;
250
+ } finally {
251
+ if (timer !== undefined) clearTimeout(timer);
252
+ // Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
253
+ resolveTimeout?.({ kind: "timeout" });
254
+ if (abortListener && abortSignal) {
255
+ abortSignal.removeEventListener("abort", abortListener);
256
+ }
257
+ resolveAbort?.({ kind: "abort" });
258
+ }
259
+ }
260
+ }
261
+
262
+ function abortReason(signal: AbortSignal): Error {
263
+ const reason = signal.reason;
264
+ if (reason instanceof Error) return reason;
265
+ if (typeof reason === "string") return new Error(reason);
266
+ return new Error("Request was aborted");
267
+ }
@@ -0,0 +1,182 @@
1
+ import { parse as partialParse } from "partial-json";
2
+
3
+ const QUOTE = 0x22;
4
+ const BACKSLASH = 0x5c;
5
+ const U = 0x75;
6
+
7
+ // Valid chars after `\`: " \ / b f n r t u
8
+ const VALID_ESCAPE_CHAR = new Uint8Array(128);
9
+ for (const ch of '"\\/bfnrtu') VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
10
+
11
+ const CONTROL_ESCAPES: readonly string[] = (() => {
12
+ const e: string[] = [];
13
+ e[0x08] = "\\b";
14
+ e[0x09] = "\\t";
15
+ e[0x0a] = "\\n";
16
+ e[0x0c] = "\\f";
17
+ e[0x0d] = "\\r";
18
+ for (let cp = 0; cp <= 0x1f; cp++) {
19
+ e[cp] ??= `\\u${cp.toString(16).padStart(4, "0")}`;
20
+ }
21
+ return e;
22
+ })();
23
+
24
+ function isHexDigit(cp: number): boolean {
25
+ return (cp >= 0x30 && cp <= 0x39) || ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x66);
26
+ }
27
+
28
+ export function repairJson(json: string): string {
29
+ const len = json.length;
30
+ const parts: string[] = [];
31
+ let lastEmit = 0;
32
+ let inString = false;
33
+ let i = 0;
34
+
35
+ while (i < len) {
36
+ if (!inString) {
37
+ // Fast scan: skip to next quote.
38
+ while (i < len && json.charCodeAt(i) !== QUOTE) i++;
39
+ if (i >= len) break;
40
+ inString = true;
41
+ i++;
42
+ continue;
43
+ }
44
+
45
+ // Fast scan inside string: advance past chars that need no handling.
46
+ while (i < len) {
47
+ const cp = json.charCodeAt(i);
48
+ if (cp < 0x20 || cp === QUOTE || cp === BACKSLASH) break;
49
+ i++;
50
+ }
51
+ if (i >= len) break;
52
+
53
+ const cp = json.charCodeAt(i);
54
+
55
+ if (cp === QUOTE) {
56
+ inString = false;
57
+ i++;
58
+ continue;
59
+ }
60
+
61
+ if (cp === BACKSLASH) {
62
+ // Need at least one char after the backslash; treat EOI as invalid escape.
63
+ if (i + 1 >= len) {
64
+ parts.push(json.slice(lastEmit, i), "\\\\");
65
+ lastEmit = i + 1;
66
+ i++;
67
+ continue;
68
+ }
69
+
70
+ const nextCp = json.charCodeAt(i + 1);
71
+
72
+ if (nextCp === U) {
73
+ // Need full \uXXXX, all four digits, all hex.
74
+ if (
75
+ i + 5 < len &&
76
+ isHexDigit(json.charCodeAt(i + 2)) &&
77
+ isHexDigit(json.charCodeAt(i + 3)) &&
78
+ isHexDigit(json.charCodeAt(i + 4)) &&
79
+ isHexDigit(json.charCodeAt(i + 5))
80
+ ) {
81
+ i += 6;
82
+ continue;
83
+ }
84
+ // Truncated or non-hex \u — escape the backslash, re-process the rest.
85
+ parts.push(json.slice(lastEmit, i), "\\\\");
86
+ lastEmit = i + 1;
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ if (nextCp < 128 && VALID_ESCAPE_CHAR[nextCp] === 1) {
92
+ i += 2;
93
+ continue;
94
+ }
95
+
96
+ parts.push(json.slice(lastEmit, i), "\\\\");
97
+ lastEmit = i + 1;
98
+ i++;
99
+ continue;
100
+ }
101
+
102
+ // Control character (cp < 0x20).
103
+ parts.push(json.slice(lastEmit, i), CONTROL_ESCAPES[cp]);
104
+ lastEmit = i + 1;
105
+ i++;
106
+ }
107
+
108
+ if (!parts.length) return json;
109
+ if (lastEmit < len) parts.push(json.slice(lastEmit));
110
+ return parts.join("");
111
+ }
112
+
113
+ export function parseJsonWithRepair<T>(json: string): T {
114
+ try {
115
+ return JSON.parse(json) as T;
116
+ } catch (error) {
117
+ const repairedJson = repairJson(json);
118
+ if (repairedJson !== json) {
119
+ return JSON.parse(repairedJson) as T;
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Attempts to parse potentially incomplete JSON during streaming.
127
+ * Always returns a valid object, even if the JSON is incomplete.
128
+ *
129
+ * @param partialJson The partial JSON string from streaming
130
+ * @returns Parsed object or empty object if parsing fails
131
+ */
132
+ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
133
+ partialJson = partialJson?.trimStart();
134
+ if (!partialJson) {
135
+ return {} as T;
136
+ }
137
+ try {
138
+ return JSON.parse(partialJson) as T;
139
+ } catch {
140
+ partialJson = repairJson(partialJson);
141
+ try {
142
+ return (partialParse(partialJson) ?? {}) as T;
143
+ } catch {
144
+ // If all parsing fails, return empty object
145
+ return {} as T;
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Default minimum byte growth before `parseStreamingJsonThrottled` will
152
+ * re-parse a streaming tool-call argument buffer. Bounds the mid-stream
153
+ * partial-parse cost from quadratic to linear in N.
154
+ */
155
+ export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
156
+
157
+ /**
158
+ * Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
159
+ *
160
+ * Tool calls arrive as a long sequence of small deltas — calling
161
+ * `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
162
+ * each time, giving O(N²) work in the total buffer length. Throttling skips
163
+ * the re-parse until at least `minGrowthBytes` of new content has arrived
164
+ * since the last successful parse, bounding mid-stream cost to O(N).
165
+ *
166
+ * Each provider tracks the last parsed length on its tool-call block, so the
167
+ * final `toolcall_end` parse (which providers already perform unconditionally)
168
+ * is the authoritative full parse — the throttle only delays mid-stream UI
169
+ * updates by at most `minGrowthBytes` of accumulated partial content.
170
+ *
171
+ * @returns the parsed object plus the new `parsedLen` to persist; or `null`
172
+ * when the buffer has not grown enough to warrant a re-parse.
173
+ */
174
+ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
175
+ partialJson: string | undefined,
176
+ lastParsedLen: number,
177
+ minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
178
+ ): { value: T; parsedLen: number } | null {
179
+ const len = partialJson?.length ?? 0;
180
+ if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
181
+ return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
182
+ }
@@ -0,0 +1,107 @@
1
+ import { afterEach, describe, expect, it, vi } from "bun:test";
2
+ import { isXAIAccessTokenExpiring, refreshXAIOAuthToken, validateXAIEndpoint, XAIOAuthFlow } from "../xai-oauth";
3
+
4
+ const originalFetch = global.fetch;
5
+
6
+ afterEach(() => {
7
+ global.fetch = originalFetch;
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ function jwtWithExp(exp: number): string {
12
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
13
+ const payload = Buffer.from(JSON.stringify({ exp })).toString("base64url");
14
+ return `${header}.${payload}.sig`;
15
+ }
16
+
17
+ describe("isXAIAccessTokenExpiring", () => {
18
+ it("returns false for an empty string", () => {
19
+ expect(isXAIAccessTokenExpiring("")).toBe(false);
20
+ });
21
+
22
+ it("returns false for a non-JWT", () => {
23
+ expect(isXAIAccessTokenExpiring("not.a.jwt")).toBe(false);
24
+ });
25
+
26
+ it("returns true when exp is already in the past", () => {
27
+ const now = Math.floor(Date.now() / 1000);
28
+ expect(isXAIAccessTokenExpiring(jwtWithExp(now - 60))).toBe(true);
29
+ });
30
+
31
+ it("returns false when exp is well in the future", () => {
32
+ const now = Math.floor(Date.now() / 1000);
33
+ expect(isXAIAccessTokenExpiring(jwtWithExp(now + 3600))).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("validateXAIEndpoint", () => {
38
+ it("rejects non-HTTPS URLs", () => {
39
+ expect(() => validateXAIEndpoint("http://x.ai/token", "token_endpoint")).toThrow(/Invalid xAI token_endpoint/);
40
+ });
41
+
42
+ it("rejects non-xAI hosts", () => {
43
+ expect(() => validateXAIEndpoint("https://evil.com/token", "token_endpoint")).toThrow(
44
+ /Invalid xAI token_endpoint/,
45
+ );
46
+ });
47
+
48
+ it("accepts the x.ai apex and *.x.ai subdomains", () => {
49
+ expect(validateXAIEndpoint("https://x.ai/token", "token_endpoint")).toBe("https://x.ai/token");
50
+ expect(validateXAIEndpoint("https://auth.x.ai/oauth/token", "token_endpoint")).toBe(
51
+ "https://auth.x.ai/oauth/token",
52
+ );
53
+ });
54
+ });
55
+
56
+ describe("refreshXAIOAuthToken", () => {
57
+ it("rejects an empty refresh_token without making a network call", async () => {
58
+ const fetchMock = vi.fn(async () => {
59
+ throw new Error("fetch should not be called when refresh_token is empty");
60
+ });
61
+ global.fetch = fetchMock as unknown as typeof fetch;
62
+
63
+ await expect(refreshXAIOAuthToken("")).rejects.toThrow(/missing refresh_token/);
64
+ expect(fetchMock).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
68
+ describe("XAIOAuthFlow", () => {
69
+ it("pins the redirect URI to xAI's allowlisted loopback port", () => {
70
+ const flow = new XAIOAuthFlow({});
71
+
72
+ expect(flow.redirectUri).toBe("http://127.0.0.1:56121/callback");
73
+ });
74
+ });
75
+
76
+ describe("XAIOAuthFlow.exchangeToken", () => {
77
+ it("rejects when the token-exchange response is missing access_token", async () => {
78
+ const fetchMock = vi.fn(async (input: string | URL) => {
79
+ const url = typeof input === "string" ? input : input.toString();
80
+ if (url.includes("/.well-known/openid-configuration")) {
81
+ return new Response(
82
+ JSON.stringify({
83
+ authorization_endpoint: "https://auth.x.ai/oauth/authorize",
84
+ token_endpoint: "https://auth.x.ai/oauth/token",
85
+ }),
86
+ { status: 200, headers: { "Content-Type": "application/json" } },
87
+ );
88
+ }
89
+ // Token-exchange response deliberately omits `access_token` to exercise
90
+ // the missing-token rejection path. The value of `refresh_token` here is
91
+ // a literal test marker, not a real secret — the test verifies
92
+ // exchangeToken throws before any token would be persisted.
93
+ return new Response(JSON.stringify({ refresh_token: "stub-refresh-token-for-test-only" }), {
94
+ status: 200,
95
+ headers: { "Content-Type": "application/json" },
96
+ });
97
+ });
98
+ global.fetch = fetchMock as unknown as typeof fetch;
99
+
100
+ const flow = new XAIOAuthFlow({});
101
+ await flow.generateAuthUrl("state-abc", "http://127.0.0.1:56121/callback");
102
+
103
+ await expect(flow.exchangeToken("code-xyz", "state-abc", "http://127.0.0.1:56121/callback")).rejects.toThrow(
104
+ /access_token/,
105
+ );
106
+ });
107
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Alibaba Coding Plan login flow.
3
+ *
4
+ * Alibaba Coding Plan provides OpenAI-compatible models via https://coding-intl.dashscope.aliyuncs.com/v1.
5
+ *
6
+ * This is not OAuth - it's a simple API key flow:
7
+ * 1. Open browser to Alibaba Cloud DashScope API key settings
8
+ * 2. User copies their API key
9
+ * 3. User pastes the API key into the CLI
10
+ */
11
+
12
+ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
13
+ import type { OAuthController } from "./types";
14
+
15
+ const AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
16
+ const API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
17
+ const VALIDATION_MODEL = "qwen3.5-plus";
18
+
19
+ /**
20
+ * Login to Alibaba Coding Plan.
21
+ *
22
+ * Opens browser to API keys page, prompts user to paste their API key.
23
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
24
+ */
25
+ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<string> {
26
+ if (!options.onPrompt) {
27
+ throw new Error("Alibaba Coding Plan login requires onPrompt callback");
28
+ }
29
+
30
+ options.onAuth?.({
31
+ url: AUTH_URL,
32
+ instructions: "Copy your API key from the Alibaba Cloud DashScope console",
33
+ });
34
+
35
+ const apiKey = await options.onPrompt({
36
+ message: "Paste your Alibaba Coding Plan API key",
37
+ placeholder: "sk-...",
38
+ });
39
+
40
+ if (options.signal?.aborted) {
41
+ throw new Error("Login cancelled");
42
+ }
43
+
44
+ const trimmed = apiKey.trim();
45
+ if (!trimmed) {
46
+ throw new Error("API key is required");
47
+ }
48
+
49
+ options.onProgress?.("Validating API key...");
50
+ await validateOpenAICompatibleApiKey({
51
+ provider: "Alibaba Coding Plan",
52
+ apiKey: trimmed,
53
+ baseUrl: API_BASE_URL,
54
+ model: VALIDATION_MODEL,
55
+ signal: options.signal,
56
+ });
57
+
58
+ return trimmed;
59
+ }