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
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Tests for batch auto-submit error scenarios
3
+ *
4
+ * RED PHASE: These tests expose error handling gaps in BatchQueue's auto-submit feature.
5
+ *
6
+ * The problem: When auto-submit triggers on maxItems threshold, errors from submit()
7
+ * are caught and only logged (line 250: `this.submit().catch(console.error)`).
8
+ * This means callers have no way to know the batch submission failed.
9
+ *
10
+ * Test scenarios:
11
+ * - Network failure during batch submit
12
+ * - Rate limit errors from API
13
+ * - Partial batch success/failure
14
+ * - Timeout during submission
15
+ *
16
+ * @see primitives.org.ai-7au
17
+ */
18
+
19
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
20
+ import {
21
+ createBatch,
22
+ BatchQueue,
23
+ registerBatchAdapter,
24
+ type BatchAdapter,
25
+ type BatchItem,
26
+ type BatchQueueOptions,
27
+ type BatchResult,
28
+ type BatchJob,
29
+ type BatchSubmitResult,
30
+ } from '../src/batch-queue.js'
31
+
32
+ // ============================================================================
33
+ // Test Helpers
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Create a mock adapter that fails on submit
38
+ */
39
+ function createFailingAdapter(error: Error): BatchAdapter {
40
+ return {
41
+ async submit(): Promise<BatchSubmitResult> {
42
+ throw error
43
+ },
44
+ async getStatus(batchId: string): Promise<BatchJob> {
45
+ return {
46
+ id: batchId,
47
+ provider: 'openai',
48
+ status: 'failed',
49
+ totalItems: 0,
50
+ completedItems: 0,
51
+ failedItems: 0,
52
+ createdAt: new Date(),
53
+ }
54
+ },
55
+ async cancel(): Promise<void> {},
56
+ async getResults(): Promise<BatchResult[]> {
57
+ return []
58
+ },
59
+ async waitForCompletion(): Promise<BatchResult[]> {
60
+ return []
61
+ },
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Create a mock adapter that succeeds
67
+ */
68
+ function createSuccessAdapter(): BatchAdapter {
69
+ let batchCounter = 0
70
+ return {
71
+ async submit(items: BatchItem[], options: BatchQueueOptions): Promise<BatchSubmitResult> {
72
+ const batchId = `batch_test_${++batchCounter}`
73
+ const results: BatchResult[] = items.map((item) => ({
74
+ id: item.id,
75
+ customId: item.id,
76
+ status: 'completed' as const,
77
+ result: `Result for ${item.prompt}`,
78
+ }))
79
+ return {
80
+ job: {
81
+ id: batchId,
82
+ provider: options.provider || 'openai',
83
+ status: 'completed',
84
+ totalItems: items.length,
85
+ completedItems: items.length,
86
+ failedItems: 0,
87
+ createdAt: new Date(),
88
+ completedAt: new Date(),
89
+ },
90
+ completion: Promise.resolve(results),
91
+ }
92
+ },
93
+ async getStatus(batchId: string): Promise<BatchJob> {
94
+ return {
95
+ id: batchId,
96
+ provider: 'openai',
97
+ status: 'completed',
98
+ totalItems: 0,
99
+ completedItems: 0,
100
+ failedItems: 0,
101
+ createdAt: new Date(),
102
+ }
103
+ },
104
+ async cancel(): Promise<void> {},
105
+ async getResults(): Promise<BatchResult[]> {
106
+ return []
107
+ },
108
+ async waitForCompletion(): Promise<BatchResult[]> {
109
+ return []
110
+ },
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Create an adapter that times out
116
+ */
117
+ function createTimeoutAdapter(timeoutMs: number): BatchAdapter {
118
+ return {
119
+ async submit(): Promise<BatchSubmitResult> {
120
+ await new Promise((_, reject) =>
121
+ setTimeout(() => reject(new Error('Request timeout')), timeoutMs)
122
+ )
123
+ throw new Error('Request timeout')
124
+ },
125
+ async getStatus(batchId: string): Promise<BatchJob> {
126
+ return {
127
+ id: batchId,
128
+ provider: 'openai',
129
+ status: 'failed',
130
+ totalItems: 0,
131
+ completedItems: 0,
132
+ failedItems: 0,
133
+ createdAt: new Date(),
134
+ }
135
+ },
136
+ async cancel(): Promise<void> {},
137
+ async getResults(): Promise<BatchResult[]> {
138
+ return []
139
+ },
140
+ async waitForCompletion(): Promise<BatchResult[]> {
141
+ return []
142
+ },
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Create an adapter that returns rate limit error
148
+ */
149
+ function createRateLimitAdapter(): BatchAdapter {
150
+ return createFailingAdapter(
151
+ Object.assign(new Error('Rate limit exceeded'), {
152
+ status: 429,
153
+ headers: { 'retry-after': '60' },
154
+ })
155
+ )
156
+ }
157
+
158
+ // ============================================================================
159
+ // Tests
160
+ // ============================================================================
161
+
162
+ describe('Batch auto-submit error handling', () => {
163
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
164
+
165
+ beforeEach(() => {
166
+ // Capture console.error to verify errors are logged
167
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
168
+ })
169
+
170
+ afterEach(() => {
171
+ consoleErrorSpy.mockRestore()
172
+ })
173
+
174
+ describe('Network failure during auto-submit', () => {
175
+ it('should emit an error event when auto-submit fails due to network error', async () => {
176
+ // Register failing adapter
177
+ const networkError = new Error('Network connection failed')
178
+ registerBatchAdapter('openai', createFailingAdapter(networkError))
179
+
180
+ const errorHandler = vi.fn()
181
+ const batch = createBatch({
182
+ provider: 'openai',
183
+ autoSubmit: true,
184
+ maxItems: 3,
185
+ })
186
+
187
+ // Subscribe to error events (this is what we expect to exist)
188
+ // This will fail because BatchQueue doesn't emit events
189
+ if ('on' in batch) {
190
+ ;(batch as BatchQueue & { on: (event: string, handler: (e: Error) => void) => void }).on(
191
+ 'error',
192
+ errorHandler
193
+ )
194
+ }
195
+
196
+ // Add items to trigger auto-submit
197
+ batch.add('prompt 1')
198
+ batch.add('prompt 2')
199
+ batch.add('prompt 3') // This should trigger auto-submit
200
+
201
+ // Wait for async auto-submit to complete
202
+ await new Promise((resolve) => setTimeout(resolve, 100))
203
+
204
+ // FAILING: Currently errors are swallowed, errorHandler never called
205
+ // The error should be propagated to the error handler
206
+ expect(errorHandler).toHaveBeenCalledWith(networkError)
207
+ })
208
+
209
+ it('should reject pending item promises when auto-submit fails', async () => {
210
+ const networkError = new Error('Network connection failed')
211
+ registerBatchAdapter('openai', createFailingAdapter(networkError))
212
+
213
+ const batch = createBatch({
214
+ provider: 'openai',
215
+ autoSubmit: true,
216
+ maxItems: 3,
217
+ })
218
+
219
+ // Get item references before auto-submit triggers
220
+ const item1 = batch.add('prompt 1')
221
+ const item2 = batch.add('prompt 2')
222
+ const item3 = batch.add('prompt 3') // Triggers auto-submit
223
+
224
+ // Wait for async auto-submit to complete
225
+ await new Promise((resolve) => setTimeout(resolve, 100))
226
+
227
+ // FAILING: Items should have error status after failed auto-submit
228
+ // Currently they remain in 'pending' status with no indication of failure
229
+ expect(item1.status).toBe('failed')
230
+ expect(item1.error).toBe('Network connection failed')
231
+ expect(item2.status).toBe('failed')
232
+ expect(item3.status).toBe('failed')
233
+ })
234
+
235
+ it('should provide a way to await auto-submit completion or failure', async () => {
236
+ const networkError = new Error('Network connection failed')
237
+ registerBatchAdapter('openai', createFailingAdapter(networkError))
238
+
239
+ const batch = createBatch({
240
+ provider: 'openai',
241
+ autoSubmit: true,
242
+ maxItems: 3,
243
+ })
244
+
245
+ batch.add('prompt 1')
246
+ batch.add('prompt 2')
247
+ batch.add('prompt 3') // Triggers auto-submit
248
+
249
+ // FAILING: There should be a way to await the auto-submit result
250
+ // Currently the submission is fire-and-forget with no way to await it
251
+ // Expected: batch.awaitAutoSubmit() or batch.getSubmissionPromise()
252
+
253
+ // This property should exist to allow awaiting auto-submit
254
+ expect('autoSubmitPromise' in batch).toBe(true)
255
+
256
+ // The promise should be available for awaiting
257
+ const autoSubmitPromise = (batch as BatchQueue & { autoSubmitPromise?: Promise<void> })
258
+ .autoSubmitPromise
259
+ expect(autoSubmitPromise).toBeDefined()
260
+
261
+ // Awaiting it should surface the error
262
+ await expect(autoSubmitPromise).rejects.toThrow('Network connection failed')
263
+ })
264
+ })
265
+
266
+ describe('Rate limit errors during auto-submit', () => {
267
+ it('should expose rate limit errors to callers', async () => {
268
+ registerBatchAdapter('openai', createRateLimitAdapter())
269
+
270
+ const batch = createBatch({
271
+ provider: 'openai',
272
+ autoSubmit: true,
273
+ maxItems: 2,
274
+ })
275
+
276
+ batch.add('prompt 1')
277
+ batch.add('prompt 2') // Triggers auto-submit
278
+
279
+ await new Promise((resolve) => setTimeout(resolve, 100))
280
+
281
+ // FAILING: Rate limit error should be exposed to caller
282
+ // Currently it's only logged to console.error
283
+ expect(consoleErrorSpy).toHaveBeenCalled()
284
+
285
+ // There should be a way to check for submission errors
286
+ // This property doesn't exist - that's the gap
287
+ const submissionError = (batch as BatchQueue & { submissionError?: Error }).submissionError
288
+ expect(submissionError).toBeDefined()
289
+ expect(submissionError?.message).toContain('Rate limit')
290
+ })
291
+
292
+ it('should include retry-after information in rate limit errors', async () => {
293
+ registerBatchAdapter('openai', createRateLimitAdapter())
294
+
295
+ const batch = createBatch({
296
+ provider: 'openai',
297
+ autoSubmit: true,
298
+ maxItems: 2,
299
+ })
300
+
301
+ batch.add('prompt 1')
302
+ batch.add('prompt 2')
303
+
304
+ await new Promise((resolve) => setTimeout(resolve, 100))
305
+
306
+ // FAILING: Rate limit metadata should be accessible
307
+ const job = batch.getJob()
308
+ // Job should contain rate limit retry info
309
+ expect(job).toBeDefined()
310
+ expect((job as BatchJob & { retryAfter?: number })?.retryAfter).toBeDefined()
311
+ })
312
+ })
313
+
314
+ describe('Timeout during auto-submit', () => {
315
+ it('should handle submission timeouts gracefully', async () => {
316
+ registerBatchAdapter('openai', createTimeoutAdapter(50))
317
+
318
+ const batch = createBatch({
319
+ provider: 'openai',
320
+ autoSubmit: true,
321
+ maxItems: 2,
322
+ })
323
+
324
+ batch.add('prompt 1')
325
+ batch.add('prompt 2')
326
+
327
+ // Wait for timeout to occur
328
+ await new Promise((resolve) => setTimeout(resolve, 200))
329
+
330
+ // FAILING: Timeout error should be captured and accessible
331
+ // Logger calls with format: 'Batch auto-submit failed:', error
332
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Batch auto-submit failed:', expect.any(Error))
333
+
334
+ // Items should reflect the failure
335
+ const items = batch.getItems()
336
+ expect(items[0].status).toBe('failed')
337
+ expect(items[0].error).toContain('timeout')
338
+ })
339
+ })
340
+
341
+ describe('Error recovery scenarios', () => {
342
+ it('should allow retry after auto-submit failure', async () => {
343
+ const failingAdapter = createFailingAdapter(new Error('Temporary failure'))
344
+ registerBatchAdapter('openai', failingAdapter)
345
+
346
+ const batch = createBatch({
347
+ provider: 'openai',
348
+ autoSubmit: true,
349
+ maxItems: 2,
350
+ })
351
+
352
+ batch.add('prompt 1')
353
+ batch.add('prompt 2') // Triggers auto-submit (fails)
354
+
355
+ await new Promise((resolve) => setTimeout(resolve, 100))
356
+
357
+ // Replace with working adapter
358
+ registerBatchAdapter('openai', createSuccessAdapter())
359
+
360
+ // FAILING: There should be a way to retry failed auto-submit
361
+ // Currently once auto-submit fires and fails, the batch is stuck
362
+ const retry = () => {
363
+ if ('retry' in batch) {
364
+ return (batch as BatchQueue & { retry: () => Promise<void> }).retry()
365
+ }
366
+ return Promise.reject(new Error('No retry method available'))
367
+ }
368
+
369
+ // Should be able to retry the submission
370
+ await expect(retry()).resolves.not.toThrow()
371
+ })
372
+
373
+ it('should reset submission state on failure to allow manual submit', async () => {
374
+ registerBatchAdapter('openai', createFailingAdapter(new Error('Submit failed')))
375
+
376
+ const batch = createBatch({
377
+ provider: 'openai',
378
+ autoSubmit: true,
379
+ maxItems: 2,
380
+ })
381
+
382
+ batch.add('prompt 1')
383
+ batch.add('prompt 2') // Triggers auto-submit (fails)
384
+
385
+ await new Promise((resolve) => setTimeout(resolve, 100))
386
+
387
+ // Replace with working adapter
388
+ registerBatchAdapter('openai', createSuccessAdapter())
389
+
390
+ // FAILING: After auto-submit failure, manual submit should be possible
391
+ // Currently isSubmitted is set to true before the async submit completes
392
+ // so we can't retry with manual submit()
393
+ expect(batch.isSubmitted).toBe(false) // Should be false after failed auto-submit
394
+
395
+ // Manual submit should work after failed auto-submit
396
+ await expect(batch.submit()).resolves.toBeDefined()
397
+ })
398
+ })
399
+
400
+ describe('Partial batch failure during auto-submit', () => {
401
+ it('should emit error event when some items fail during auto-submit', async () => {
402
+ // Create adapter that fails some items
403
+ const partialAdapter: BatchAdapter = {
404
+ async submit(items: BatchItem[]): Promise<BatchSubmitResult> {
405
+ const results: BatchResult[] = items.map((item, i) => ({
406
+ id: item.id,
407
+ customId: item.id,
408
+ status: i % 2 === 0 ? 'completed' : 'failed',
409
+ result: i % 2 === 0 ? `Result for ${item.prompt}` : undefined,
410
+ error: i % 2 === 1 ? 'Processing failed' : undefined,
411
+ }))
412
+ return {
413
+ job: {
414
+ id: 'batch_partial',
415
+ provider: 'openai',
416
+ status: 'completed',
417
+ totalItems: items.length,
418
+ completedItems: results.filter((r) => r.status === 'completed').length,
419
+ failedItems: results.filter((r) => r.status === 'failed').length,
420
+ createdAt: new Date(),
421
+ },
422
+ completion: Promise.resolve(results),
423
+ }
424
+ },
425
+ async getStatus(batchId: string): Promise<BatchJob> {
426
+ return {
427
+ id: batchId,
428
+ provider: 'openai',
429
+ status: 'completed',
430
+ totalItems: 4,
431
+ completedItems: 2,
432
+ failedItems: 2,
433
+ createdAt: new Date(),
434
+ }
435
+ },
436
+ async cancel(): Promise<void> {},
437
+ async getResults(): Promise<BatchResult[]> {
438
+ return []
439
+ },
440
+ async waitForCompletion(): Promise<BatchResult[]> {
441
+ return []
442
+ },
443
+ }
444
+
445
+ registerBatchAdapter('openai', partialAdapter)
446
+
447
+ const partialFailureHandler = vi.fn()
448
+ const batch = createBatch({
449
+ provider: 'openai',
450
+ autoSubmit: true,
451
+ maxItems: 4,
452
+ })
453
+
454
+ // FAILING: There should be a way to subscribe to partial failure events
455
+ // This tests that callers can be notified when some items fail
456
+ if ('on' in batch) {
457
+ ;(
458
+ batch as BatchQueue & {
459
+ on: (event: string, handler: (results: BatchResult[]) => void) => void
460
+ }
461
+ ).on('partial-failure', partialFailureHandler)
462
+ }
463
+
464
+ batch.add('prompt 1')
465
+ batch.add('prompt 2')
466
+ batch.add('prompt 3')
467
+ batch.add('prompt 4') // Triggers auto-submit
468
+
469
+ // Wait for auto-submit to complete
470
+ await new Promise((resolve) => setTimeout(resolve, 100))
471
+
472
+ // FAILING: Partial failure handler should be called with failed items
473
+ expect(partialFailureHandler).toHaveBeenCalled()
474
+ expect(partialFailureHandler).toHaveBeenCalledWith(
475
+ expect.arrayContaining([
476
+ expect.objectContaining({ status: 'failed', error: 'Processing failed' }),
477
+ ])
478
+ )
479
+ })
480
+
481
+ it('should provide aggregated error info after partial auto-submit failure', async () => {
482
+ const partialAdapter: BatchAdapter = {
483
+ async submit(items: BatchItem[]): Promise<BatchSubmitResult> {
484
+ const results: BatchResult[] = items.map((item, i) => ({
485
+ id: item.id,
486
+ customId: item.id,
487
+ status: i % 2 === 0 ? 'completed' : 'failed',
488
+ result: i % 2 === 0 ? `Result for ${item.prompt}` : undefined,
489
+ error: i % 2 === 1 ? 'Processing failed' : undefined,
490
+ }))
491
+ return {
492
+ job: {
493
+ id: 'batch_partial',
494
+ provider: 'openai',
495
+ status: 'completed',
496
+ totalItems: items.length,
497
+ completedItems: results.filter((r) => r.status === 'completed').length,
498
+ failedItems: results.filter((r) => r.status === 'failed').length,
499
+ createdAt: new Date(),
500
+ },
501
+ completion: Promise.resolve(results),
502
+ }
503
+ },
504
+ async getStatus(batchId: string): Promise<BatchJob> {
505
+ return {
506
+ id: batchId,
507
+ provider: 'openai',
508
+ status: 'completed',
509
+ totalItems: 4,
510
+ completedItems: 2,
511
+ failedItems: 2,
512
+ createdAt: new Date(),
513
+ }
514
+ },
515
+ async cancel(): Promise<void> {},
516
+ async getResults(): Promise<BatchResult[]> {
517
+ return []
518
+ },
519
+ async waitForCompletion(): Promise<BatchResult[]> {
520
+ return []
521
+ },
522
+ }
523
+
524
+ registerBatchAdapter('openai', partialAdapter)
525
+
526
+ const batch = createBatch({
527
+ provider: 'openai',
528
+ autoSubmit: true,
529
+ maxItems: 4,
530
+ })
531
+
532
+ batch.add('prompt 1')
533
+ batch.add('prompt 2')
534
+ batch.add('prompt 3')
535
+ batch.add('prompt 4') // Triggers auto-submit
536
+
537
+ await new Promise((resolve) => setTimeout(resolve, 100))
538
+
539
+ // FAILING: There should be a way to get failure summary
540
+ const failedItems = (
541
+ batch as BatchQueue & { getFailedItems?: () => BatchItem[] }
542
+ ).getFailedItems?.()
543
+ expect(failedItems).toBeDefined()
544
+ expect(failedItems?.length).toBe(2)
545
+ })
546
+ })
547
+
548
+ describe('Console.error verification (current behavior)', () => {
549
+ it('verifies errors are currently only logged, not propagated', async () => {
550
+ const testError = new Error('Test submission error')
551
+ registerBatchAdapter('openai', createFailingAdapter(testError))
552
+
553
+ const batch = createBatch({
554
+ provider: 'openai',
555
+ autoSubmit: true,
556
+ maxItems: 2,
557
+ })
558
+
559
+ batch.add('prompt 1')
560
+ batch.add('prompt 2') // Triggers auto-submit
561
+
562
+ await new Promise((resolve) => setTimeout(resolve, 100))
563
+
564
+ // This passes - errors ARE logged
565
+ // Logger calls with format: 'Batch auto-submit failed:', error
566
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Batch auto-submit failed:', testError)
567
+
568
+ // But there's no other way to access the error
569
+ // - No error event emitted
570
+ // - No error property on batch
571
+ // - No way to await the auto-submit
572
+ // - Items remain in 'pending' state
573
+
574
+ const items = batch.getItems()
575
+ // FAILING: Items should not remain pending after failed submission
576
+ expect(items[0].status).not.toBe('pending')
577
+ })
578
+ })
579
+ })
580
+
581
+ describe('Suggested API improvements', () => {
582
+ it('documents expected error handling API', () => {
583
+ // This test documents what the error handling API SHOULD look like
584
+ // All these assertions will fail, showing the gaps
585
+
586
+ const batch = createBatch({
587
+ provider: 'openai',
588
+ autoSubmit: true,
589
+ maxItems: 5,
590
+ })
591
+
592
+ // 1. Event-based error handling
593
+ expect('on' in batch).toBe(true)
594
+ expect(typeof (batch as unknown as { on?: unknown }).on).toBe('function')
595
+
596
+ // 2. Promise-based error handling
597
+ expect('awaitAutoSubmit' in batch).toBe(true)
598
+ expect(typeof (batch as unknown as { awaitAutoSubmit?: unknown }).awaitAutoSubmit).toBe(
599
+ 'function'
600
+ )
601
+
602
+ // 3. Error state inspection
603
+ expect('submissionError' in batch).toBe(true)
604
+ expect('hasSubmissionError' in batch).toBe(true)
605
+
606
+ // 4. Retry capability
607
+ expect('retry' in batch).toBe(true)
608
+ expect(typeof (batch as unknown as { retry?: unknown }).retry).toBe('function')
609
+ })
610
+ })