ai-functions 2.1.1 → 2.3.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 (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +54 -837
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
package/src/retry.ts ADDED
@@ -0,0 +1,902 @@
1
+ /**
2
+ * Retry and fallback patterns for AI function calls
3
+ *
4
+ * Provides:
5
+ * - Exponential backoff with configurable base delay and multiplier
6
+ * - Jitter to prevent thundering herd (equal, full, decorrelated strategies)
7
+ * - Circuit breaker for fail-fast behavior after repeated failures
8
+ * - Fallback chains for model failover (sonnet -> opus -> gpt-4o)
9
+ * - Error classification for intelligent retry decisions
10
+ * - Partial retry for batch operations
11
+ *
12
+ * Per-model policy data (which models retry how, who falls back to whom,
13
+ * which batch tiers each model supports) lives in `language-models`'s
14
+ * `policyFor()`. The classes in this file are the *machinery* that reads
15
+ * that policy. See `RetryPolicy.forModel`, `CircuitBreaker.forModel`,
16
+ * `FallbackChain.forModel`.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+
21
+ import { policyFor, type ModelPolicy, type ErrorCategoryName } from 'language-models'
22
+
23
+ // ============================================================================
24
+ // ERROR TYPES AND CLASSIFICATION
25
+ // ============================================================================
26
+
27
+ /**
28
+ * Error categories for retry decision making
29
+ */
30
+ export enum ErrorCategory {
31
+ /** Network connectivity issues (retryable) */
32
+ Network = 'network',
33
+ /** Rate limiting / quota exceeded (retryable with backoff) */
34
+ RateLimit = 'rate_limit',
35
+ /** Invalid input / bad request (not retryable) */
36
+ InvalidInput = 'invalid_input',
37
+ /** Authentication / authorization errors (not retryable) */
38
+ Authentication = 'authentication',
39
+ /** Server errors (retryable) */
40
+ Server = 'server',
41
+ /** Context length exceeded (not retryable without modification) */
42
+ ContextLength = 'context_length',
43
+ /** Unknown error type */
44
+ Unknown = 'unknown',
45
+ }
46
+
47
+ /**
48
+ * Base class for retryable errors
49
+ */
50
+ export class RetryableError extends Error {
51
+ readonly retryable = true
52
+ readonly category: ErrorCategory
53
+
54
+ constructor(message: string, category: ErrorCategory = ErrorCategory.Unknown) {
55
+ super(message)
56
+ this.name = 'RetryableError'
57
+ this.category = category
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Base class for non-retryable errors
63
+ */
64
+ export class NonRetryableError extends Error {
65
+ readonly retryable = false
66
+ readonly category: ErrorCategory
67
+
68
+ constructor(message: string, category: ErrorCategory = ErrorCategory.InvalidInput) {
69
+ super(message)
70
+ this.name = 'NonRetryableError'
71
+ this.category = category
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Network-related errors (connection issues, timeouts)
77
+ */
78
+ export class NetworkError extends RetryableError {
79
+ constructor(message: string) {
80
+ super(message, ErrorCategory.Network)
81
+ this.name = 'NetworkError'
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Rate limit errors with optional retry-after
87
+ */
88
+ export class RateLimitError extends RetryableError {
89
+ readonly retryAfter?: number
90
+
91
+ constructor(message: string, options?: { retryAfter?: number }) {
92
+ super(message, ErrorCategory.RateLimit)
93
+ this.name = 'RateLimitError'
94
+ if (options?.retryAfter !== undefined) {
95
+ this.retryAfter = options.retryAfter
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Create RateLimitError from HTTP response
101
+ */
102
+ static fromResponse(response: {
103
+ status: number
104
+ headers?: Record<string, string>
105
+ }): RateLimitError {
106
+ const retryAfterHeader = response.headers?.['retry-after']
107
+ let retryAfter: number | undefined
108
+
109
+ if (retryAfterHeader) {
110
+ const seconds = parseInt(retryAfterHeader, 10)
111
+ if (!isNaN(seconds)) {
112
+ retryAfter = seconds * 1000 // Convert to milliseconds
113
+ }
114
+ }
115
+
116
+ return new RateLimitError(
117
+ `Rate limited (${response.status})`,
118
+ retryAfter !== undefined ? { retryAfter } : undefined
119
+ )
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Error thrown when circuit breaker is open
125
+ */
126
+ export class CircuitOpenError extends Error {
127
+ readonly retryable = false
128
+
129
+ constructor(message: string = 'Circuit breaker is open') {
130
+ super(message)
131
+ this.name = 'CircuitOpenError'
132
+ }
133
+ }
134
+
135
+ /** Error with status property (e.g., HTTP errors) */
136
+ interface ErrorWithStatus extends Error {
137
+ status?: number
138
+ }
139
+
140
+ /**
141
+ * Classify an error into a category for retry decisions
142
+ */
143
+ export function classifyError(error: unknown): ErrorCategory {
144
+ if (!(error instanceof Error)) {
145
+ return ErrorCategory.Unknown
146
+ }
147
+
148
+ const message = error.message.toLowerCase()
149
+ const status = (error as ErrorWithStatus).status
150
+
151
+ // Network errors
152
+ if (
153
+ message.includes('econnrefused') ||
154
+ message.includes('etimedout') ||
155
+ message.includes('enotfound') ||
156
+ message.includes('socket hang up') ||
157
+ message.includes('network request failed') ||
158
+ message.includes('fetch failed')
159
+ ) {
160
+ return ErrorCategory.Network
161
+ }
162
+
163
+ // Rate limit errors
164
+ if (
165
+ message.includes('rate limit') ||
166
+ message.includes('429') ||
167
+ message.includes('too many requests') ||
168
+ message.includes('quota exceeded') ||
169
+ status === 429
170
+ ) {
171
+ return ErrorCategory.RateLimit
172
+ }
173
+
174
+ // Invalid input errors
175
+ if (
176
+ message.includes('invalid json') ||
177
+ message.includes('400 bad request') ||
178
+ message.includes('validation failed') ||
179
+ status === 400 ||
180
+ status === 422
181
+ ) {
182
+ return ErrorCategory.InvalidInput
183
+ }
184
+
185
+ // Authentication errors
186
+ if (
187
+ message.includes('401 unauthorized') ||
188
+ message.includes('403 forbidden') ||
189
+ message.includes('invalid api key') ||
190
+ status === 401 ||
191
+ status === 403
192
+ ) {
193
+ return ErrorCategory.Authentication
194
+ }
195
+
196
+ // Server errors
197
+ if (
198
+ message.includes('500') ||
199
+ message.includes('502') ||
200
+ message.includes('503') ||
201
+ message.includes('504') ||
202
+ message.includes('internal server error') ||
203
+ message.includes('bad gateway') ||
204
+ message.includes('service unavailable') ||
205
+ message.includes('gateway timeout') ||
206
+ (status && status >= 500 && status < 600)
207
+ ) {
208
+ return ErrorCategory.Server
209
+ }
210
+
211
+ // Context length errors
212
+ if (
213
+ message.includes('context length') ||
214
+ message.includes('token limit') ||
215
+ message.includes('maximum context')
216
+ ) {
217
+ return ErrorCategory.ContextLength
218
+ }
219
+
220
+ return ErrorCategory.Unknown
221
+ }
222
+
223
+ // ============================================================================
224
+ // BACKOFF CALCULATION
225
+ // ============================================================================
226
+
227
+ /**
228
+ * Jitter strategy for backoff calculation
229
+ */
230
+ export type JitterStrategy = 'equal' | 'full' | 'decorrelated'
231
+
232
+ /**
233
+ * Options for backoff calculation
234
+ */
235
+ export interface BackoffOptions {
236
+ /** Base delay in milliseconds (default: 1000) */
237
+ baseDelay?: number
238
+ /** Maximum delay cap in milliseconds (default: 30000) */
239
+ maxDelay?: number
240
+ /** Exponential multiplier (default: 2) */
241
+ multiplier?: number
242
+ /** Jitter factor 0-1 for equal jitter (default: 0) */
243
+ jitter?: number
244
+ /** Jitter strategy (default: 'equal') */
245
+ jitterStrategy?: JitterStrategy
246
+ /** Previous delay for decorrelated jitter */
247
+ previousDelay?: number
248
+ }
249
+
250
+ /**
251
+ * Calculate backoff delay with exponential increase and optional jitter
252
+ *
253
+ * @param attempt - Current attempt number (0-indexed)
254
+ * @param options - Backoff configuration
255
+ * @returns Delay in milliseconds
256
+ */
257
+ export function calculateBackoff(attempt: number, options: BackoffOptions = {}): number {
258
+ const {
259
+ baseDelay = 1000,
260
+ maxDelay = 30000,
261
+ multiplier = 2,
262
+ jitter = 0,
263
+ jitterStrategy = 'equal',
264
+ previousDelay,
265
+ } = options
266
+
267
+ // Calculate base exponential delay
268
+ let delay = baseDelay * Math.pow(multiplier, attempt)
269
+
270
+ // Apply jitter based on strategy
271
+ if (jitterStrategy === 'full') {
272
+ // Full jitter: random value between 0 and calculated delay
273
+ delay = Math.random() * delay
274
+ } else if (jitterStrategy === 'decorrelated' && previousDelay !== undefined) {
275
+ // Decorrelated jitter: random between baseDelay and previousDelay * 3
276
+ delay = baseDelay + Math.random() * (previousDelay * 3 - baseDelay)
277
+ } else if (jitter > 0) {
278
+ // Equal jitter: +/- jitter% of calculated delay
279
+ const jitterRange = delay * jitter
280
+ delay = delay - jitterRange + Math.random() * 2 * jitterRange
281
+ }
282
+
283
+ // Apply max delay cap
284
+ return Math.min(delay, maxDelay)
285
+ }
286
+
287
+ // ============================================================================
288
+ // RETRY POLICY
289
+ // ============================================================================
290
+
291
+ /**
292
+ * Options for retry policy
293
+ */
294
+ export interface RetryOptions {
295
+ /** Maximum number of retries (default: 3) */
296
+ maxRetries?: number
297
+ /** Base delay in milliseconds (default: 1000) */
298
+ baseDelay?: number
299
+ /** Maximum delay cap in milliseconds (default: 30000) */
300
+ maxDelay?: number
301
+ /** Exponential multiplier (default: 2) */
302
+ multiplier?: number
303
+ /** Jitter factor 0-1 (default: 0) */
304
+ jitter?: number
305
+ /** Jitter strategy (default: 'equal') */
306
+ jitterStrategy?: JitterStrategy
307
+ /** Respect retry-after headers from rate limit errors (default: true) */
308
+ respectRetryAfter?: boolean
309
+ /** Custom function to determine if error is retryable */
310
+ shouldRetry?: (error: unknown) => boolean
311
+ }
312
+
313
+ /**
314
+ * Info passed to operations during retry
315
+ */
316
+ export interface RetryInfo {
317
+ attempt: number
318
+ maxRetries: number
319
+ }
320
+
321
+ /**
322
+ * Result of a batch item operation
323
+ */
324
+ export interface BatchItemResult<T, R> {
325
+ success: boolean
326
+ item: T
327
+ result?: R
328
+ error?: Error
329
+ }
330
+
331
+ /**
332
+ * Retry policy for executing operations with exponential backoff
333
+ *
334
+ * @deprecated Phase C Week 3 — `RetryPolicy` has 1 real production caller
335
+ * (audited 2026-05-06; see `bd show aip-ibid`):
336
+ * `ai-database/src/cascade-orchestrator.ts:1235` (loose coupling — dynamic
337
+ * import + graceful try/catch fallback when ai-functions not available).
338
+ * AI SDK 6's `customProvider({ retryPolicy })` and `wrapLanguageModel(model,
339
+ * retryMiddleware)` cover the same surface. Migration tracked in aip-ibid;
340
+ * the one callsite can move on a separate commit. Will be removed in the
341
+ * Phase C semver bump.
342
+ */
343
+ export class RetryPolicy {
344
+ private readonly options: Required<Omit<RetryOptions, 'shouldRetry'>> & {
345
+ shouldRetry?: (error: unknown) => boolean
346
+ }
347
+
348
+ constructor(options: RetryOptions = {}) {
349
+ this.options = {
350
+ maxRetries: options.maxRetries ?? 3,
351
+ baseDelay: options.baseDelay ?? 1000,
352
+ maxDelay: options.maxDelay ?? 30000,
353
+ multiplier: options.multiplier ?? 2,
354
+ jitter: options.jitter ?? 0,
355
+ jitterStrategy: options.jitterStrategy ?? 'equal',
356
+ respectRetryAfter: options.respectRetryAfter ?? true,
357
+ ...(options.shouldRetry !== undefined && { shouldRetry: options.shouldRetry }),
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Build a RetryPolicy from a model's `ModelPolicy` (loaded via
363
+ * `language-models`). Per-call `overrides` win over policy data.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const policy = RetryPolicy.forModel('sonnet')
368
+ * // Uses retry settings derived for anthropic/claude-sonnet-4.5
369
+ * ```
370
+ */
371
+ static forModel(alias: string, overrides: RetryOptions = {}): RetryPolicy {
372
+ const policy = policyFor(alias)
373
+ return RetryPolicy.fromPolicy(policy, overrides)
374
+ }
375
+
376
+ /**
377
+ * Build a RetryPolicy directly from a `ModelPolicy`. Useful when the policy
378
+ * is already in hand (e.g. from a request context).
379
+ */
380
+ static fromPolicy(policy: ModelPolicy, overrides: RetryOptions = {}): RetryPolicy {
381
+ const retryable = new Set<ErrorCategoryName>(policy.retry.retryableCategories)
382
+ const shouldRetry = (error: unknown): boolean => {
383
+ // Honour error's own retryable property when present.
384
+ if (error && typeof error === 'object' && 'retryable' in error) {
385
+ const flag = (error as { retryable: boolean }).retryable
386
+ if (flag === false) return false
387
+ }
388
+ const category = classifyError(error)
389
+ return retryable.has(category as ErrorCategoryName)
390
+ }
391
+ return new RetryPolicy({
392
+ maxRetries: policy.retry.maxRetries,
393
+ baseDelay: policy.retry.baseDelay,
394
+ maxDelay: policy.retry.maxDelay,
395
+ multiplier: policy.retry.multiplier,
396
+ jitter: policy.retry.jitter,
397
+ shouldRetry,
398
+ ...overrides,
399
+ })
400
+ }
401
+
402
+ /**
403
+ * Execute an operation with retry logic
404
+ */
405
+ async execute<T>(operation: (info: RetryInfo) => Promise<T>): Promise<T> {
406
+ let lastError: unknown
407
+ let previousDelay = this.options.baseDelay
408
+
409
+ for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
410
+ try {
411
+ return await operation({ attempt, maxRetries: this.options.maxRetries })
412
+ } catch (error) {
413
+ lastError = error
414
+
415
+ // Check if error is retryable
416
+ if (!this.isRetryable(error)) {
417
+ throw error
418
+ }
419
+
420
+ // Don't wait after the last attempt
421
+ if (attempt === this.options.maxRetries) {
422
+ break
423
+ }
424
+
425
+ // Calculate delay
426
+ let delay = calculateBackoff(attempt, {
427
+ baseDelay: this.options.baseDelay,
428
+ maxDelay: this.options.maxDelay,
429
+ multiplier: this.options.multiplier,
430
+ jitter: this.options.jitter,
431
+ jitterStrategy: this.options.jitterStrategy,
432
+ previousDelay,
433
+ })
434
+
435
+ // Respect retry-after for rate limit errors
436
+ if (this.options.respectRetryAfter && error instanceof RateLimitError && error.retryAfter) {
437
+ delay = error.retryAfter
438
+ }
439
+
440
+ previousDelay = delay
441
+ await this.sleep(delay)
442
+ }
443
+ }
444
+
445
+ throw lastError
446
+ }
447
+
448
+ /**
449
+ * Execute a batch operation with partial retry for failed items
450
+ */
451
+ async executeBatch<T, R>(
452
+ items: T[],
453
+ batchProcessor: (items: T[]) => Promise<BatchItemResult<T, R>[]>
454
+ ): Promise<BatchItemResult<T, R>[]> {
455
+ const finalResults = new Map<T, BatchItemResult<T, R>>()
456
+ let pendingItems = [...items]
457
+ const attemptCounts = new Map<T, number>()
458
+
459
+ // Initialize attempt counts
460
+ items.forEach((item) => attemptCounts.set(item, 0))
461
+
462
+ for (let round = 0; round <= this.options.maxRetries && pendingItems.length > 0; round++) {
463
+ // Wait before retry (not on first attempt)
464
+ if (round > 0) {
465
+ const delay = calculateBackoff(round - 1, {
466
+ baseDelay: this.options.baseDelay,
467
+ maxDelay: this.options.maxDelay,
468
+ multiplier: this.options.multiplier,
469
+ jitter: this.options.jitter,
470
+ jitterStrategy: this.options.jitterStrategy,
471
+ })
472
+ await this.sleep(delay)
473
+ }
474
+
475
+ // Process current batch
476
+ const results = await batchProcessor(pendingItems)
477
+
478
+ // Separate successful and failed items
479
+ const failedItems: T[] = []
480
+
481
+ for (const result of results) {
482
+ attemptCounts.set(result.item, (attemptCounts.get(result.item) || 0) + 1)
483
+
484
+ if (result.success) {
485
+ finalResults.set(result.item, result)
486
+ } else {
487
+ // Check if we can retry this item
488
+ const attempts = attemptCounts.get(result.item) || 0
489
+ if (attempts <= this.options.maxRetries && this.isRetryable(result.error)) {
490
+ failedItems.push(result.item)
491
+ } else {
492
+ finalResults.set(result.item, result)
493
+ }
494
+ }
495
+ }
496
+
497
+ pendingItems = failedItems
498
+ }
499
+
500
+ // Return results in original order
501
+ return items.map((item) => finalResults.get(item)!)
502
+ }
503
+
504
+ private isRetryable(error: unknown): boolean {
505
+ // Check custom shouldRetry function first
506
+ if (this.options.shouldRetry) {
507
+ return this.options.shouldRetry(error)
508
+ }
509
+
510
+ // Check error's own retryable property
511
+ if (error && typeof error === 'object' && 'retryable' in error) {
512
+ return (error as { retryable: boolean }).retryable === true
513
+ }
514
+
515
+ // Classify error and determine retryability
516
+ const category = classifyError(error)
517
+ return (
518
+ category === ErrorCategory.Network ||
519
+ category === ErrorCategory.RateLimit ||
520
+ category === ErrorCategory.Server ||
521
+ category === ErrorCategory.Unknown
522
+ )
523
+ }
524
+
525
+ private sleep(ms: number): Promise<void> {
526
+ return new Promise((resolve) => setTimeout(resolve, ms))
527
+ }
528
+ }
529
+
530
+ // ============================================================================
531
+ // CIRCUIT BREAKER
532
+ // ============================================================================
533
+
534
+ /**
535
+ * Circuit breaker state
536
+ */
537
+ export type CircuitState = 'closed' | 'open' | 'half-open'
538
+
539
+ /**
540
+ * Options for circuit breaker
541
+ */
542
+ export interface CircuitBreakerOptions {
543
+ /** Number of failures before opening circuit (default: 5) */
544
+ failureThreshold?: number
545
+ /** Time in ms before transitioning to half-open (default: 30000) */
546
+ resetTimeout?: number
547
+ /** Number of successful calls to close circuit (default: 1) */
548
+ successThreshold?: number
549
+ }
550
+
551
+ /**
552
+ * Circuit breaker metrics
553
+ */
554
+ export interface CircuitBreakerMetrics {
555
+ state: CircuitState
556
+ failureCount: number
557
+ successCount: number
558
+ lastFailure: Date | null
559
+ lastSuccess: Date | null
560
+ totalFailures: number
561
+ totalSuccesses: number
562
+ }
563
+
564
+ /**
565
+ * Circuit breaker for fail-fast behavior
566
+ *
567
+ * States:
568
+ * - CLOSED: Normal operation, failures tracked
569
+ * - OPEN: Fail fast, reject all requests
570
+ * - HALF-OPEN: Allow single test request
571
+ *
572
+ * @deprecated Phase C Week 3 — `CircuitBreaker` has zero real callers in
573
+ * primitives.org.ai (audited 2026-05-06; only comment-only references in
574
+ * `language-models/src/index.ts`; see `bd show aip-ibid`). AI SDK 6's
575
+ * `wrapLanguageModel(model, circuitMiddleware)` replacement is the going-
576
+ * forward primitive. Will be removed in the Phase C semver bump.
577
+ */
578
+ export class CircuitBreaker {
579
+ private _state: CircuitState = 'closed'
580
+ private _failureCount = 0
581
+ private _successCount = 0
582
+ private _lastFailure: Date | null = null
583
+ private _lastSuccess: Date | null = null
584
+ private _totalFailures = 0
585
+ private _totalSuccesses = 0
586
+ private _openedAt: number | null = null
587
+ private readonly options: Required<CircuitBreakerOptions>
588
+
589
+ constructor(options: CircuitBreakerOptions = {}) {
590
+ this.options = {
591
+ failureThreshold: options.failureThreshold ?? 5,
592
+ resetTimeout: options.resetTimeout ?? 30000,
593
+ successThreshold: options.successThreshold ?? 1,
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Build a CircuitBreaker for a specific model, using its `ModelPolicy`.
599
+ * Per-call overrides win over policy data.
600
+ */
601
+ static forModel(alias: string, overrides: CircuitBreakerOptions = {}): CircuitBreaker {
602
+ const policy = policyFor(alias)
603
+ return new CircuitBreaker({
604
+ failureThreshold: policy.circuitBreaker.failureThreshold,
605
+ resetTimeout: policy.circuitBreaker.resetTimeout,
606
+ successThreshold: policy.circuitBreaker.successThreshold,
607
+ ...overrides,
608
+ })
609
+ }
610
+
611
+ /**
612
+ * Current circuit state
613
+ */
614
+ get state(): CircuitState {
615
+ // Check if we should transition from open to half-open
616
+ if (this._state === 'open' && this._openedAt !== null) {
617
+ if (Date.now() - this._openedAt >= this.options.resetTimeout) {
618
+ this._state = 'half-open'
619
+ }
620
+ }
621
+ return this._state
622
+ }
623
+
624
+ /**
625
+ * Current failure count
626
+ */
627
+ get failureCount(): number {
628
+ return this._failureCount
629
+ }
630
+
631
+ /**
632
+ * Execute an operation through the circuit breaker
633
+ */
634
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
635
+ // Check current state
636
+ const currentState = this.state
637
+
638
+ if (currentState === 'open') {
639
+ throw new CircuitOpenError()
640
+ }
641
+
642
+ try {
643
+ const result = await operation()
644
+ this.recordSuccess()
645
+ return result
646
+ } catch (error) {
647
+ this.recordFailure()
648
+ throw error
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Record a successful operation
654
+ */
655
+ private recordSuccess(): void {
656
+ this._successCount++
657
+ this._totalSuccesses++
658
+ this._lastSuccess = new Date()
659
+ this._failureCount = 0 // Reset failure count on success
660
+
661
+ if (this._state === 'half-open') {
662
+ if (this._successCount >= this.options.successThreshold) {
663
+ this._state = 'closed'
664
+ this._openedAt = null
665
+ }
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Record a failed operation
671
+ */
672
+ private recordFailure(): void {
673
+ this._failureCount++
674
+ this._totalFailures++
675
+ this._lastFailure = new Date()
676
+ this._successCount = 0 // Reset success count on failure
677
+
678
+ if (this._state === 'closed') {
679
+ if (this._failureCount >= this.options.failureThreshold) {
680
+ this._state = 'open'
681
+ this._openedAt = Date.now()
682
+ }
683
+ } else if (this._state === 'half-open') {
684
+ // Any failure in half-open state reopens the circuit
685
+ this._state = 'open'
686
+ this._openedAt = Date.now()
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Get circuit breaker metrics
692
+ */
693
+ getMetrics(): CircuitBreakerMetrics {
694
+ return {
695
+ state: this.state,
696
+ failureCount: this._failureCount,
697
+ successCount: this._successCount,
698
+ lastFailure: this._lastFailure,
699
+ lastSuccess: this._lastSuccess,
700
+ totalFailures: this._totalFailures,
701
+ totalSuccesses: this._totalSuccesses,
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Manually reset the circuit breaker
707
+ */
708
+ reset(): void {
709
+ this._state = 'closed'
710
+ this._failureCount = 0
711
+ this._successCount = 0
712
+ this._openedAt = null
713
+ }
714
+ }
715
+
716
+ // ============================================================================
717
+ // FALLBACK CHAIN
718
+ // ============================================================================
719
+
720
+ /**
721
+ * A model in the fallback chain
722
+ */
723
+ export interface FallbackModel<T = unknown, P = unknown> {
724
+ name: string
725
+ execute: (params?: P) => Promise<T>
726
+ }
727
+
728
+ /**
729
+ * Options for fallback chain
730
+ */
731
+ export interface FallbackOptions {
732
+ /** Custom function to determine if fallback should be attempted */
733
+ shouldFallback?: (error: unknown) => boolean
734
+ }
735
+
736
+ /**
737
+ * Metrics from fallback chain execution
738
+ */
739
+ export interface FallbackMetrics {
740
+ attempts: number
741
+ successfulModel: string | null
742
+ failedModels: string[]
743
+ totalDuration: number
744
+ errors: Array<{ model: string; error: Error }>
745
+ }
746
+
747
+ /**
748
+ * Fallback chain for model failover
749
+ *
750
+ * Tries models in order until one succeeds:
751
+ * sonnet -> opus -> gpt-4o -> gemini
752
+ *
753
+ * @deprecated Phase C Week 3 — `FallbackChain` (LLM model failover) has
754
+ * zero real callers in primitives.org.ai (audited 2026-05-06; the
755
+ * `human-in-the-loop` package's `FallbackChain` is a different class for
756
+ * HITL fallback resolution, not LLM failover). AI SDK 4.3+ ships native
757
+ * `customProvider({ fallbackProvider })` which is the going-forward
758
+ * primitive. See `bd show aip-ibid`. Will be removed in the Phase C
759
+ * semver bump.
760
+ */
761
+ export class FallbackChain<T = unknown, P = unknown> {
762
+ private readonly models: FallbackModel<T, P>[]
763
+ private readonly options: FallbackOptions
764
+ private lastMetrics: FallbackMetrics | null = null
765
+
766
+ constructor(models: FallbackModel<T, P>[], options: FallbackOptions = {}) {
767
+ if (models.length === 0) {
768
+ throw new Error('FallbackChain requires at least one model')
769
+ }
770
+ this.models = models
771
+ this.options = options
772
+ }
773
+
774
+ /**
775
+ * Build a FallbackChain from a model's `ModelPolicy`. The caller supplies
776
+ * an `executor` that takes a model id and returns a promise — the chain
777
+ * applies it to the primary model first, then to each fallback in order.
778
+ *
779
+ * @example
780
+ * ```ts
781
+ * const chain = FallbackChain.forModel('sonnet', (modelId, params) =>
782
+ * ai({ model: modelId, prompt: params!.prompt })
783
+ * )
784
+ * await chain.execute({ prompt: 'Hello' })
785
+ * ```
786
+ */
787
+ static forModel<T = unknown, P = unknown>(
788
+ alias: string,
789
+ executor: (modelId: string, params?: P) => Promise<T>,
790
+ options: FallbackOptions = {}
791
+ ): FallbackChain<T, P> {
792
+ const policy = policyFor(alias)
793
+ const ids = [policy.$id, ...policy.fallbackChain]
794
+ const models: FallbackModel<T, P>[] = ids.map((id) => ({
795
+ name: id,
796
+ execute: (params?: P) => executor(id, params),
797
+ }))
798
+ return new FallbackChain<T, P>(models, options)
799
+ }
800
+
801
+ /**
802
+ * Execute the fallback chain
803
+ */
804
+ async execute(params?: P): Promise<T> {
805
+ const startTime = Date.now()
806
+ const failedModels: string[] = []
807
+ const errors: Array<{ model: string; error: Error }> = []
808
+
809
+ for (const model of this.models) {
810
+ try {
811
+ const result = await model.execute(params)
812
+
813
+ this.lastMetrics = {
814
+ attempts: failedModels.length + 1,
815
+ successfulModel: model.name,
816
+ failedModels,
817
+ totalDuration: Date.now() - startTime,
818
+ errors,
819
+ }
820
+
821
+ return result
822
+ } catch (error) {
823
+ const err = error instanceof Error ? error : new Error(String(error))
824
+ failedModels.push(model.name)
825
+ errors.push({ model: model.name, error: err })
826
+
827
+ // Check if we should attempt fallback
828
+ if (this.options.shouldFallback && !this.options.shouldFallback(error)) {
829
+ this.lastMetrics = {
830
+ attempts: failedModels.length,
831
+ successfulModel: null,
832
+ failedModels,
833
+ totalDuration: Date.now() - startTime,
834
+ errors,
835
+ }
836
+ throw error
837
+ }
838
+ }
839
+ }
840
+
841
+ this.lastMetrics = {
842
+ attempts: this.models.length,
843
+ successfulModel: null,
844
+ failedModels,
845
+ totalDuration: Date.now() - startTime,
846
+ errors,
847
+ }
848
+
849
+ throw new Error('All fallback models failed')
850
+ }
851
+
852
+ /**
853
+ * Get metrics from the last execution
854
+ */
855
+ getMetrics(): FallbackMetrics {
856
+ if (!this.lastMetrics) {
857
+ return {
858
+ attempts: 0,
859
+ successfulModel: null,
860
+ failedModels: [],
861
+ totalDuration: 0,
862
+ errors: [],
863
+ }
864
+ }
865
+ return this.lastMetrics
866
+ }
867
+ }
868
+
869
+ // ============================================================================
870
+ // CONVENIENCE HELPER
871
+ // ============================================================================
872
+
873
+ /**
874
+ * Wrap an async function with retry logic
875
+ *
876
+ * @example
877
+ * ```ts
878
+ * const reliableFetch = withRetry(fetch, {
879
+ * maxRetries: 3,
880
+ * baseDelay: 1000,
881
+ * jitter: 0.2,
882
+ * })
883
+ *
884
+ * const response = await reliableFetch('https://api.example.com')
885
+ * ```
886
+ */
887
+ export function withRetry<TArgs extends unknown[], TResult>(
888
+ fn: (...args: TArgs) => Promise<TResult>,
889
+ options: RetryOptions = {}
890
+ ): (...args: TArgs) => Promise<TResult> {
891
+ const policy = new RetryPolicy(options)
892
+
893
+ return async (...args: TArgs): Promise<TResult> => {
894
+ return policy.execute(() => fn(...args))
895
+ }
896
+ }
897
+
898
+ // ============================================================================
899
+ // EXPORTS
900
+ // ============================================================================
901
+
902
+ export type { RetryOptions as RetryPolicyOptions }