@earendil-works/pi-ai 0.81.0 → 0.82.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 (281) hide show
  1. package/README.md +42 -4
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +26 -10
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +21 -8
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +17 -10
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/constrained-sampling.d.ts +17 -0
  12. package/dist/api/constrained-sampling.d.ts.map +1 -0
  13. package/dist/api/constrained-sampling.js +100 -0
  14. package/dist/api/constrained-sampling.js.map +1 -0
  15. package/dist/api/google-generative-ai.d.ts.map +1 -1
  16. package/dist/api/google-generative-ai.js +7 -11
  17. package/dist/api/google-generative-ai.js.map +1 -1
  18. package/dist/api/google-shared.d.ts +4 -3
  19. package/dist/api/google-shared.d.ts.map +1 -1
  20. package/dist/api/google-shared.js +17 -3
  21. package/dist/api/google-shared.js.map +1 -1
  22. package/dist/api/google-vertex.d.ts.map +1 -1
  23. package/dist/api/google-vertex.js +7 -11
  24. package/dist/api/google-vertex.js.map +1 -1
  25. package/dist/api/mistral-conversations.d.ts.map +1 -1
  26. package/dist/api/mistral-conversations.js +13 -9
  27. package/dist/api/mistral-conversations.js.map +1 -1
  28. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  29. package/dist/api/openai-codex-responses.js +68 -27
  30. package/dist/api/openai-codex-responses.js.map +1 -1
  31. package/dist/api/openai-completions.d.ts +6 -7
  32. package/dist/api/openai-completions.d.ts.map +1 -1
  33. package/dist/api/openai-completions.js +131 -34
  34. package/dist/api/openai-completions.js.map +1 -1
  35. package/dist/api/openai-responses-shared.d.ts +5 -0
  36. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  37. package/dist/api/openai-responses-shared.js +173 -66
  38. package/dist/api/openai-responses-shared.js.map +1 -1
  39. package/dist/api/openai-responses.d.ts.map +1 -1
  40. package/dist/api/openai-responses.js +29 -7
  41. package/dist/api/openai-responses.js.map +1 -1
  42. package/dist/api/openrouter-images.d.ts.map +1 -1
  43. package/dist/api/openrouter-images.js +8 -3
  44. package/dist/api/openrouter-images.js.map +1 -1
  45. package/dist/auth/oauth/kimi-coding.d.ts +10 -0
  46. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -0
  47. package/dist/auth/oauth/kimi-coding.js +249 -0
  48. package/dist/auth/oauth/kimi-coding.js.map +1 -0
  49. package/dist/auth/oauth/load.d.ts +4 -0
  50. package/dist/auth/oauth/load.d.ts.map +1 -1
  51. package/dist/auth/oauth/load.js +10 -0
  52. package/dist/auth/oauth/load.js.map +1 -1
  53. package/dist/auth/oauth/openrouter.d.ts +13 -0
  54. package/dist/auth/oauth/openrouter.d.ts.map +1 -0
  55. package/dist/auth/oauth/openrouter.js +222 -0
  56. package/dist/auth/oauth/openrouter.js.map +1 -0
  57. package/dist/bun-oauth.d.ts.map +1 -1
  58. package/dist/bun-oauth.js +4 -0
  59. package/dist/bun-oauth.js.map +1 -1
  60. package/dist/image-models.generated.d.ts +15 -0
  61. package/dist/image-models.generated.d.ts.map +1 -1
  62. package/dist/image-models.generated.js +15 -0
  63. package/dist/image-models.generated.js.map +1 -1
  64. package/dist/model-catalog.d.ts +17 -0
  65. package/dist/model-catalog.d.ts.map +1 -0
  66. package/dist/model-catalog.js +4 -0
  67. package/dist/model-catalog.js.map +1 -0
  68. package/dist/models.generated.d.ts +74 -4434
  69. package/dist/models.generated.d.ts.map +1 -1
  70. package/dist/models.generated.js.map +1 -1
  71. package/dist/providers/all.d.ts +2 -2
  72. package/dist/providers/all.d.ts.map +1 -1
  73. package/dist/providers/all.js +5 -3
  74. package/dist/providers/all.js.map +1 -1
  75. package/dist/providers/amazon-bedrock.models.d.ts +3 -439
  76. package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
  77. package/dist/providers/amazon-bedrock.models.js +2 -1
  78. package/dist/providers/amazon-bedrock.models.js.map +1 -1
  79. package/dist/providers/ant-ling.models.d.ts +3 -15
  80. package/dist/providers/ant-ling.models.d.ts.map +1 -1
  81. package/dist/providers/ant-ling.models.js +2 -1
  82. package/dist/providers/ant-ling.models.js.map +1 -1
  83. package/dist/providers/anthropic.models.d.ts +3 -59
  84. package/dist/providers/anthropic.models.d.ts.map +1 -1
  85. package/dist/providers/anthropic.models.js +2 -1
  86. package/dist/providers/anthropic.models.js.map +1 -1
  87. package/dist/providers/azure-openai-responses.models.d.ts +3 -187
  88. package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  89. package/dist/providers/azure-openai-responses.models.js +2 -1
  90. package/dist/providers/azure-openai-responses.models.js.map +1 -1
  91. package/dist/providers/cerebras.models.d.ts +3 -15
  92. package/dist/providers/cerebras.models.d.ts.map +1 -1
  93. package/dist/providers/cerebras.models.js +2 -1
  94. package/dist/providers/cerebras.models.js.map +1 -1
  95. package/dist/providers/cloudflare-ai-gateway.models.d.ts +3 -171
  96. package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
  97. package/dist/providers/cloudflare-ai-gateway.models.js +2 -1
  98. package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  99. package/dist/providers/cloudflare-workers-ai.models.d.ts +3 -55
  100. package/dist/providers/cloudflare-workers-ai.models.d.ts.map +1 -1
  101. package/dist/providers/cloudflare-workers-ai.models.js +2 -1
  102. package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
  103. package/dist/providers/data/.manifest.json +1 -1
  104. package/dist/providers/data/amazon-bedrock.json +1 -1
  105. package/dist/providers/data/ant-ling.json +1 -1
  106. package/dist/providers/data/anthropic.json +1 -1
  107. package/dist/providers/data/azure-openai-responses.json +1 -1
  108. package/dist/providers/data/cerebras.json +1 -1
  109. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  110. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  111. package/dist/providers/data/deepseek.json +1 -1
  112. package/dist/providers/data/fireworks.json +1 -1
  113. package/dist/providers/data/github-copilot.json +1 -1
  114. package/dist/providers/data/google-vertex.json +1 -1
  115. package/dist/providers/data/google.json +1 -1
  116. package/dist/providers/data/groq.json +1 -1
  117. package/dist/providers/data/huggingface.json +1 -1
  118. package/dist/providers/data/kimi-coding.json +1 -1
  119. package/dist/providers/data/minimax-cn.json +1 -1
  120. package/dist/providers/data/minimax.json +1 -1
  121. package/dist/providers/data/mistral.json +1 -1
  122. package/dist/providers/data/moonshotai-cn.json +1 -1
  123. package/dist/providers/data/moonshotai.json +1 -1
  124. package/dist/providers/data/nvidia.json +1 -1
  125. package/dist/providers/data/openai-codex.json +1 -1
  126. package/dist/providers/data/openai.json +1 -1
  127. package/dist/providers/data/opencode-go.json +1 -1
  128. package/dist/providers/data/opencode.json +1 -1
  129. package/dist/providers/data/openrouter.json +1 -1
  130. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  131. package/dist/providers/data/qwen-token-plan.json +1 -1
  132. package/dist/providers/data/together.json +1 -1
  133. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  134. package/dist/providers/data/xai.json +1 -1
  135. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  136. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  137. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  138. package/dist/providers/data/xiaomi.json +1 -1
  139. package/dist/providers/data/zai-coding-cn.json +1 -1
  140. package/dist/providers/data/zai.json +1 -1
  141. package/dist/providers/deepseek.models.d.ts +3 -11
  142. package/dist/providers/deepseek.models.d.ts.map +1 -1
  143. package/dist/providers/deepseek.models.js +2 -1
  144. package/dist/providers/deepseek.models.js.map +1 -1
  145. package/dist/providers/fireworks.models.d.ts +3 -67
  146. package/dist/providers/fireworks.models.d.ts.map +1 -1
  147. package/dist/providers/fireworks.models.js +2 -1
  148. package/dist/providers/fireworks.models.js.map +1 -1
  149. package/dist/providers/github-copilot.models.d.ts +3 -115
  150. package/dist/providers/github-copilot.models.d.ts.map +1 -1
  151. package/dist/providers/github-copilot.models.js +2 -1
  152. package/dist/providers/github-copilot.models.js.map +1 -1
  153. package/dist/providers/google-vertex.models.d.ts +3 -43
  154. package/dist/providers/google-vertex.models.d.ts.map +1 -1
  155. package/dist/providers/google-vertex.models.js +2 -1
  156. package/dist/providers/google-vertex.models.js.map +1 -1
  157. package/dist/providers/google.models.d.ts +3 -67
  158. package/dist/providers/google.models.d.ts.map +1 -1
  159. package/dist/providers/google.models.js +2 -1
  160. package/dist/providers/google.models.js.map +1 -1
  161. package/dist/providers/groq.models.d.ts +3 -31
  162. package/dist/providers/groq.models.d.ts.map +1 -1
  163. package/dist/providers/groq.models.js +2 -1
  164. package/dist/providers/groq.models.js.map +1 -1
  165. package/dist/providers/huggingface.models.d.ts +3 -199
  166. package/dist/providers/huggingface.models.d.ts.map +1 -1
  167. package/dist/providers/huggingface.models.js +2 -1
  168. package/dist/providers/huggingface.models.js.map +1 -1
  169. package/dist/providers/kimi-coding.d.ts.map +1 -1
  170. package/dist/providers/kimi-coding.js +10 -2
  171. package/dist/providers/kimi-coding.js.map +1 -1
  172. package/dist/providers/kimi-coding.models.d.ts +3 -15
  173. package/dist/providers/kimi-coding.models.d.ts.map +1 -1
  174. package/dist/providers/kimi-coding.models.js +2 -1
  175. package/dist/providers/kimi-coding.models.js.map +1 -1
  176. package/dist/providers/minimax-cn.models.d.ts +3 -15
  177. package/dist/providers/minimax-cn.models.d.ts.map +1 -1
  178. package/dist/providers/minimax-cn.models.js +2 -1
  179. package/dist/providers/minimax-cn.models.js.map +1 -1
  180. package/dist/providers/minimax.models.d.ts +3 -15
  181. package/dist/providers/minimax.models.d.ts.map +1 -1
  182. package/dist/providers/minimax.models.js +2 -1
  183. package/dist/providers/minimax.models.js.map +1 -1
  184. package/dist/providers/mistral.models.d.ts +3 -123
  185. package/dist/providers/mistral.models.d.ts.map +1 -1
  186. package/dist/providers/mistral.models.js +2 -1
  187. package/dist/providers/mistral.models.js.map +1 -1
  188. package/dist/providers/moonshotai-cn.models.d.ts +3 -43
  189. package/dist/providers/moonshotai-cn.models.d.ts.map +1 -1
  190. package/dist/providers/moonshotai-cn.models.js +2 -1
  191. package/dist/providers/moonshotai-cn.models.js.map +1 -1
  192. package/dist/providers/moonshotai.models.d.ts +3 -43
  193. package/dist/providers/moonshotai.models.d.ts.map +1 -1
  194. package/dist/providers/moonshotai.models.js +2 -1
  195. package/dist/providers/moonshotai.models.js.map +1 -1
  196. package/dist/providers/nvidia.models.d.ts +3 -79
  197. package/dist/providers/nvidia.models.d.ts.map +1 -1
  198. package/dist/providers/nvidia.models.js +2 -1
  199. package/dist/providers/nvidia.models.js.map +1 -1
  200. package/dist/providers/openai-codex.models.d.ts +3 -31
  201. package/dist/providers/openai-codex.models.d.ts.map +1 -1
  202. package/dist/providers/openai-codex.models.js +2 -1
  203. package/dist/providers/openai-codex.models.js.map +1 -1
  204. package/dist/providers/openai.models.d.ts +3 -187
  205. package/dist/providers/openai.models.d.ts.map +1 -1
  206. package/dist/providers/openai.models.js +2 -1
  207. package/dist/providers/openai.models.js.map +1 -1
  208. package/dist/providers/opencode-go.models.d.ts +3 -63
  209. package/dist/providers/opencode-go.models.d.ts.map +1 -1
  210. package/dist/providers/opencode-go.models.js +2 -1
  211. package/dist/providers/opencode-go.models.js.map +1 -1
  212. package/dist/providers/opencode.models.d.ts +3 -215
  213. package/dist/providers/opencode.models.d.ts.map +1 -1
  214. package/dist/providers/opencode.models.js +2 -1
  215. package/dist/providers/opencode.models.js.map +1 -1
  216. package/dist/providers/openrouter-images.d.ts.map +1 -1
  217. package/dist/providers/openrouter-images.js +10 -2
  218. package/dist/providers/openrouter-images.js.map +1 -1
  219. package/dist/providers/openrouter.d.ts.map +1 -1
  220. package/dist/providers/openrouter.js +10 -2
  221. package/dist/providers/openrouter.js.map +1 -1
  222. package/dist/providers/openrouter.models.d.ts +3 -1079
  223. package/dist/providers/openrouter.models.d.ts.map +1 -1
  224. package/dist/providers/openrouter.models.js +2 -1
  225. package/dist/providers/openrouter.models.js.map +1 -1
  226. package/dist/providers/qwen-token-plan-cn.models.d.ts +3 -63
  227. package/dist/providers/qwen-token-plan-cn.models.d.ts.map +1 -1
  228. package/dist/providers/qwen-token-plan-cn.models.js +2 -1
  229. package/dist/providers/qwen-token-plan-cn.models.js.map +1 -1
  230. package/dist/providers/qwen-token-plan.models.d.ts +3 -63
  231. package/dist/providers/qwen-token-plan.models.d.ts.map +1 -1
  232. package/dist/providers/qwen-token-plan.models.js +2 -1
  233. package/dist/providers/qwen-token-plan.models.js.map +1 -1
  234. package/dist/providers/together.models.d.ts +3 -67
  235. package/dist/providers/together.models.d.ts.map +1 -1
  236. package/dist/providers/together.models.js +2 -1
  237. package/dist/providers/together.models.js.map +1 -1
  238. package/dist/providers/vercel-ai-gateway.models.d.ts +3 -755
  239. package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  240. package/dist/providers/vercel-ai-gateway.models.js +2 -1
  241. package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  242. package/dist/providers/xai.models.d.ts +3 -15
  243. package/dist/providers/xai.models.d.ts.map +1 -1
  244. package/dist/providers/xai.models.js +2 -1
  245. package/dist/providers/xai.models.js.map +1 -1
  246. package/dist/providers/xiaomi-token-plan-ams.models.d.ts +3 -15
  247. package/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
  248. package/dist/providers/xiaomi-token-plan-ams.models.js +2 -1
  249. package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  250. package/dist/providers/xiaomi-token-plan-cn.models.d.ts +3 -15
  251. package/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
  252. package/dist/providers/xiaomi-token-plan-cn.models.js +2 -1
  253. package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  254. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +3 -15
  255. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
  256. package/dist/providers/xiaomi-token-plan-sgp.models.js +2 -1
  257. package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  258. package/dist/providers/xiaomi.models.d.ts +3 -27
  259. package/dist/providers/xiaomi.models.d.ts.map +1 -1
  260. package/dist/providers/xiaomi.models.js +2 -1
  261. package/dist/providers/xiaomi.models.js.map +1 -1
  262. package/dist/providers/zai-coding-cn.models.d.ts +3 -27
  263. package/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
  264. package/dist/providers/zai-coding-cn.models.js +2 -1
  265. package/dist/providers/zai-coding-cn.models.js.map +1 -1
  266. package/dist/providers/zai.models.d.ts +3 -27
  267. package/dist/providers/zai.models.d.ts.map +1 -1
  268. package/dist/providers/zai.models.js +2 -1
  269. package/dist/providers/zai.models.js.map +1 -1
  270. package/dist/types.d.ts +35 -2
  271. package/dist/types.d.ts.map +1 -1
  272. package/dist/types.js.map +1 -1
  273. package/dist/utils/provider-retry.d.ts +16 -0
  274. package/dist/utils/provider-retry.d.ts.map +1 -0
  275. package/dist/utils/provider-retry.js +94 -0
  276. package/dist/utils/provider-retry.js.map +1 -0
  277. package/dist/utils/retry.d.ts +41 -0
  278. package/dist/utils/retry.d.ts.map +1 -1
  279. package/dist/utils/retry.js +82 -0
  280. package/dist/utils/retry.js.map +1 -1
  281. package/package.json +1 -1
@@ -0,0 +1,94 @@
1
+ const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
2
+ function isProviderError(error) {
3
+ if (!(error instanceof Error) || !("status" in error) || !("headers" in error))
4
+ return false;
5
+ return ((error.status === undefined || typeof error.status === "number") &&
6
+ (error.headers === undefined || error.headers instanceof Headers));
7
+ }
8
+ /** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */
9
+ function isRetryableProviderError(error) {
10
+ const shouldRetry = error.headers?.get("x-should-retry");
11
+ if (shouldRetry === "true")
12
+ return true;
13
+ if (shouldRetry === "false")
14
+ return false;
15
+ if (error.status === undefined)
16
+ return true;
17
+ return (error.status === 408 ||
18
+ error.status === 409 ||
19
+ error.status === 429 ||
20
+ (typeof error.status === "number" && error.status >= 500));
21
+ }
22
+ function validateServerRetryDelayMs(delayMs, maxRetryDelayMs, providerErrorMessage) {
23
+ const maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
24
+ if (maxDelayMs > 0 && delayMs > maxDelayMs) {
25
+ throw new Error(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`);
26
+ }
27
+ return delayMs;
28
+ }
29
+ function getRetryDelayMs(error, retryIndex, maxRetryDelayMs) {
30
+ const retryAfterMs = error.headers?.get("retry-after-ms");
31
+ if (retryAfterMs) {
32
+ const value = Number.parseFloat(retryAfterMs);
33
+ if (!Number.isNaN(value))
34
+ return validateServerRetryDelayMs(value, maxRetryDelayMs, error.message);
35
+ }
36
+ const retryAfter = error.headers?.get("retry-after");
37
+ if (retryAfter) {
38
+ const seconds = Number.parseFloat(retryAfter);
39
+ const delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
40
+ return validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);
41
+ }
42
+ const exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;
43
+ return exponentialDelay * (1 - Math.random() * 0.25);
44
+ }
45
+ function createAbortError() {
46
+ const error = new Error("Request aborted");
47
+ error.name = "AbortError";
48
+ return error;
49
+ }
50
+ function abortableSleep(ms, signal) {
51
+ return new Promise((resolve, reject) => {
52
+ if (signal?.aborted) {
53
+ reject(createAbortError());
54
+ return;
55
+ }
56
+ const onAbort = () => {
57
+ clearTimeout(timeout);
58
+ reject(createAbortError());
59
+ };
60
+ const timeout = setTimeout(() => {
61
+ signal?.removeEventListener("abort", onAbort);
62
+ resolve();
63
+ }, Math.max(0, ms));
64
+ signal?.addEventListener("abort", onAbort, { once: true });
65
+ });
66
+ }
67
+ /**
68
+ * Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making
69
+ * their backoff sleep interruptible. Their built-in retry timers ignore the
70
+ * request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and
71
+ * wrap the request with this helper. Provider-requested delays above
72
+ * `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to
73
+ * disable the limit.
74
+ */
75
+ export async function retryProviderRequest(request, options = {}) {
76
+ const maxRetries = options.maxRetries ?? 0;
77
+ let retriesRemaining = maxRetries;
78
+ for (;;) {
79
+ try {
80
+ // Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.
81
+ return await request();
82
+ }
83
+ catch (error) {
84
+ if (options.signal?.aborted)
85
+ throw createAbortError();
86
+ if (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error))
87
+ throw error;
88
+ const retryIndex = maxRetries - retriesRemaining;
89
+ retriesRemaining--;
90
+ await abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);
91
+ }
92
+ }
93
+ }
94
+ //# sourceMappingURL=provider-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-retry.js","sourceRoot":"","sources":["../../src/utils/provider-retry.ts"],"names":[],"mappings":"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","sourcesContent":["const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\n\ninterface ProviderRetryOptions {\n\tmaxRetries?: number;\n\tmaxRetryDelayMs?: number;\n\tsignal?: AbortSignal;\n}\n\ninterface ProviderError extends Error {\n\tstatus: number | undefined;\n\theaders: Headers | undefined;\n}\n\nfunction isProviderError(error: unknown): error is ProviderError {\n\tif (!(error instanceof Error) || !(\"status\" in error) || !(\"headers\" in error)) return false;\n\treturn (\n\t\t(error.status === undefined || typeof error.status === \"number\") &&\n\t\t(error.headers === undefined || error.headers instanceof Headers)\n\t);\n}\n\n/** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */\nfunction isRetryableProviderError(error: ProviderError): boolean {\n\tconst shouldRetry = error.headers?.get(\"x-should-retry\");\n\tif (shouldRetry === \"true\") return true;\n\tif (shouldRetry === \"false\") return false;\n\n\tif (error.status === undefined) return true;\n\treturn (\n\t\terror.status === 408 ||\n\t\terror.status === 409 ||\n\t\terror.status === 429 ||\n\t\t(typeof error.status === \"number\" && error.status >= 500)\n\t);\n}\n\nfunction validateServerRetryDelayMs(\n\tdelayMs: number,\n\tmaxRetryDelayMs: number | undefined,\n\tproviderErrorMessage: string,\n): number {\n\tconst maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;\n\tif (maxDelayMs > 0 && delayMs > maxDelayMs) {\n\t\tthrow new Error(\n\t\t\t`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`,\n\t\t);\n\t}\n\treturn delayMs;\n}\n\nfunction getRetryDelayMs(error: ProviderError, retryIndex: number, maxRetryDelayMs: number | undefined): number {\n\tconst retryAfterMs = error.headers?.get(\"retry-after-ms\");\n\tif (retryAfterMs) {\n\t\tconst value = Number.parseFloat(retryAfterMs);\n\t\tif (!Number.isNaN(value)) return validateServerRetryDelayMs(value, maxRetryDelayMs, error.message);\n\t}\n\n\tconst retryAfter = error.headers?.get(\"retry-after\");\n\tif (retryAfter) {\n\t\tconst seconds = Number.parseFloat(retryAfter);\n\t\tconst delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;\n\t\treturn validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);\n\t}\n\n\tconst exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;\n\treturn exponentialDelay * (1 - Math.random() * 0.25);\n}\n\nfunction createAbortError(): Error {\n\tconst error = new Error(\"Request aborted\");\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(createAbortError());\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(createAbortError());\n\t\t};\n\t\tconst timeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tresolve();\n\t\t\t},\n\t\t\tMath.max(0, ms),\n\t\t);\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making\n * their backoff sleep interruptible. Their built-in retry timers ignore the\n * request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and\n * wrap the request with this helper. Provider-requested delays above\n * `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to\n * disable the limit.\n */\nexport async function retryProviderRequest<T>(\n\trequest: () => Promise<T>,\n\toptions: ProviderRetryOptions = {},\n): Promise<T> {\n\tconst maxRetries = options.maxRetries ?? 0;\n\tlet retriesRemaining = maxRetries;\n\n\tfor (;;) {\n\t\ttry {\n\t\t\t// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.\n\t\t\treturn await request();\n\t\t} catch (error) {\n\t\t\tif (options.signal?.aborted) throw createAbortError();\n\t\t\tif (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error)) throw error;\n\n\t\t\tconst retryIndex = maxRetries - retriesRemaining;\n\t\t\tretriesRemaining--;\n\t\t\tawait abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);\n\t\t}\n\t}\n}\n"]}
@@ -1,4 +1,45 @@
1
1
  import type { AssistantMessage } from "../types.ts";
2
+ /**
3
+ * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).
4
+ * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept
5
+ * here so the classifier and the policy-driven retry loop live together and stay reusable
6
+ * by the SDK and other callers.
7
+ */
8
+ export interface RetryPolicy {
9
+ enabled: boolean;
10
+ /** Max retry attempts (0 = no retries). The initial call never counts as a retry. */
11
+ maxRetries: number;
12
+ /** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */
13
+ baseDelayMs: number;
14
+ }
15
+ /** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */
16
+ export interface RetryCallbacks {
17
+ /** Emitted before the backoff sleep of each retry attempt (1-indexed). */
18
+ onRetryScheduled?: (attempt: number, maxAttempts: number, delayMs: number, errorMessage: string) => void | Promise<void>;
19
+ /** Emitted after the backoff sleep, immediately before the retried call starts. */
20
+ onRetryAttemptStart?: () => void | Promise<void>;
21
+ /** Emitted once when the loop ends: success if a later call completed normally. */
22
+ onRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;
23
+ }
24
+ /**
25
+ * Run a single assistant-producing call with bounded retry on transient errors.
26
+ *
27
+ * Behavior:
28
+ * - A successful response is returned immediately. Aborts are terminal and never
29
+ * retried, but reported as unsuccessful if they happen after a retry was scheduled.
30
+ * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`
31
+ * too, so callers do not need to care when cancellation happened.
32
+ * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/
33
+ * billing exhaustion) is returned immediately so deterministic errors fail fast.
34
+ * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting
35
+ * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before
36
+ * the retried call starts, and `onRetryFinished` once at the end (whether the loop
37
+ * ends in success, exhausted retries, or an aborted backoff).
38
+ *
39
+ * When `policy` is undefined or disabled, the first response is returned unchanged
40
+ * (equivalent to calling `produce()` directly).
41
+ */
42
+ export declare function retryAssistantCall(produce: () => Promise<AssistantMessage>, policy: RetryPolicy | undefined, signal: AbortSignal | undefined, callbacks?: RetryCallbacks): Promise<AssistantMessage>;
2
43
  /**
3
44
  * Classifies whether a failed assistant message looks like a transient provider
4
45
  * or transport error, so callers can decide if the last assistant turn should be
@@ -1 +1 @@
1
- {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAuFpD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA0FpD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EACxC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
@@ -43,6 +43,9 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
43
43
  "connection.?lost",
44
44
  "other side closed",
45
45
  "fetch failed",
46
+ "getaddrinfo",
47
+ "ENOTFOUND",
48
+ "EAI_AGAIN",
46
49
  "upstream.?connect",
47
50
  "reset before headers",
48
51
  "socket hang up",
@@ -71,6 +74,85 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
71
74
  // gRPC based providers (e.g. NVIDIA NIM)
72
75
  "ResourceExhausted",
73
76
  ]);
77
+ class RetrySleepAbortError extends Error {
78
+ constructor() {
79
+ super("Aborted");
80
+ }
81
+ }
82
+ function sleep(ms, signal) {
83
+ return new Promise((resolve, reject) => {
84
+ if (signal?.aborted) {
85
+ reject(new RetrySleepAbortError());
86
+ return;
87
+ }
88
+ const timeout = setTimeout(resolve, ms);
89
+ signal?.addEventListener("abort", () => {
90
+ clearTimeout(timeout);
91
+ reject(new RetrySleepAbortError());
92
+ }, { once: true });
93
+ });
94
+ }
95
+ /**
96
+ * Run a single assistant-producing call with bounded retry on transient errors.
97
+ *
98
+ * Behavior:
99
+ * - A successful response is returned immediately. Aborts are terminal and never
100
+ * retried, but reported as unsuccessful if they happen after a retry was scheduled.
101
+ * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`
102
+ * too, so callers do not need to care when cancellation happened.
103
+ * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/
104
+ * billing exhaustion) is returned immediately so deterministic errors fail fast.
105
+ * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting
106
+ * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before
107
+ * the retried call starts, and `onRetryFinished` once at the end (whether the loop
108
+ * ends in success, exhausted retries, or an aborted backoff).
109
+ *
110
+ * When `policy` is undefined or disabled, the first response is returned unchanged
111
+ * (equivalent to calling `produce()` directly).
112
+ */
113
+ export async function retryAssistantCall(produce, policy, signal, callbacks) {
114
+ const maxAttempts = policy?.enabled ? policy.maxRetries : 0;
115
+ let attempt = 0;
116
+ let lastRetry;
117
+ for (;;) {
118
+ const response = await produce();
119
+ // Abort: terminal but not successful. Never retry an aborted message.
120
+ if (response.stopReason === "aborted") {
121
+ if (lastRetry)
122
+ await callbacks?.onRetryFinished?.(false, lastRetry.attempt);
123
+ return response;
124
+ }
125
+ // Success: non-error, non-abort responses return as-is.
126
+ if (response.stopReason !== "error") {
127
+ if (lastRetry)
128
+ await callbacks?.onRetryFinished?.(true, lastRetry.attempt);
129
+ return response;
130
+ }
131
+ // Non-retryable, or budget exhausted: return the final error message.
132
+ if (attempt >= maxAttempts || !isRetryableAssistantError(response)) {
133
+ if (lastRetry)
134
+ await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);
135
+ return response;
136
+ }
137
+ attempt++;
138
+ lastRetry = { attempt, errorMessage: response.errorMessage || "Unknown error" };
139
+ const delayMs = policy.baseDelayMs * 2 ** (attempt - 1);
140
+ await callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);
141
+ // Normalize aborts during retry backoff to the same AssistantMessage shape as
142
+ // provider stream aborts, so callers do not need to care when cancellation happened.
143
+ try {
144
+ await sleep(delayMs, signal);
145
+ }
146
+ catch (error) {
147
+ await callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);
148
+ if (error instanceof RetrySleepAbortError) {
149
+ return { ...response, stopReason: "aborted", errorMessage: undefined };
150
+ }
151
+ throw error;
152
+ }
153
+ await callbacks?.onRetryAttemptStart?.();
154
+ }
155
+ }
74
156
  /**
75
157
  * Classifies whether a failed assistant message looks like a transient provider
76
158
  * or transport error, so callers can decide if the last assistant turn should be
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAEA,SAAS,yBAAyB,CAAC,QAA2B,EAAU;IACvE,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AAAA,CAC3C;AAED,MAAM,0CAA0C,GAAG,yBAAyB,CAAC;IAC5E,8EAA8E;IAC9E,2EAA2E;IAC3E,mBAAmB;IACnB,qBAAqB;IAErB,6EAA6E;IAC7E,yDAAyD;IACzD,6BAA6B;IAC7B,mBAAmB;IAEnB,4EAA4E;IAC5E,4EAA4E;IAC5E,oBAAoB;IACpB,eAAe;IACf,gBAAgB;IAChB,SAAS;CACT,CAAC,CAAC;AAEH,MAAM,gCAAgC,GAAG,yBAAyB,CAAC;IAClE,0EAA0E;IAC1E,YAAY;IACZ,aAAa;IACb,mBAAmB;IACnB,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,sBAAsB;IACtB,eAAe;IACf,iBAAiB;IAEjB,8EAA8E;IAC9E,+CAA+C;IAC/C,2BAA2B;IAE3B,2EAA2E;IAC3E,2EAA2E;IAC3E,2EAA2E;IAC3E,gBAAgB;IAChB,mBAAmB;IACnB,qBAAqB;IACrB,kBAAkB;IAClB,mBAAmB;IACnB,cAAc;IACd,mBAAmB;IACnB,sBAAsB;IACtB,gBAAgB;IAChB,8BAA8B;IAC9B,YAAY;IACZ,SAAS;IACT,YAAY;IAEZ,+EAA+E;IAC/E,mBAAmB;IACnB,kBAAkB;IAElB,yEAAyE;IACzE,8EAA8E;IAC9E,yEAAyE;IACzE,eAAe;IACf,kCAAkC;IAClC,+CAA+C;IAC/C,sCAAsC;IAEtC,4EAA4E;IAC5E,iEAAiE;IACjE,aAAa;IAEb,6EAA6E;IAC7E,6BAA6B;IAC7B,4BAA4B;IAC5B,wBAAwB;IACxB,2BAA2B;IAE3B,yCAAyC;IACzC,mBAAmB;CACnB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAyB,EAAW;IAC7E,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAC1E,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC1C,IAAI,0CAA0C,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAC;IAChF,OAAO,gCAAgC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAAA,CAC3D","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-ai",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",