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
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Google GenAI (Gemini) Adapter
3
3
  *
4
- * Implements processing using Google's Generative AI API (Gemini models).
5
4
  * Google doesn't have a native batch API like OpenAI/Anthropic, so this
6
- * implements concurrent processing for the flex tier.
5
+ * adapter fakes batch processing via concurrent direct calls and tracks the
6
+ * job state locally (see `LocalJobStore` in `./provider.js`).
7
+ *
8
+ * For true async batch processing, consider Google Cloud Batch with Vertex AI.
7
9
  *
8
10
  * @see https://ai.google.dev/gemini-api/docs
9
11
  *
@@ -11,20 +13,22 @@
11
13
  */
12
14
 
13
15
  import {
16
+ LocalJobStore,
17
+ processConcurrently,
14
18
  registerBatchAdapter,
15
19
  registerFlexAdapter,
20
+ tryParseJson,
16
21
  type BatchAdapter,
17
- type FlexAdapter,
18
22
  type BatchItem,
19
23
  type BatchJob,
20
24
  type BatchQueueOptions,
21
25
  type BatchResult,
22
26
  type BatchSubmitResult,
23
- type BatchStatus,
24
- } from '../batch-queue.js'
27
+ type FlexAdapter,
28
+ } from './provider.js'
25
29
 
26
30
  // ============================================================================
27
- // Types
31
+ // Provider-specific types
28
32
  // ============================================================================
29
33
 
30
34
  interface GeminiMessage {
@@ -39,10 +43,7 @@ interface GeminiResponse {
39
43
  role: string
40
44
  }
41
45
  finishReason: string
42
- safetyRatings?: Array<{
43
- category: string
44
- probability: string
45
- }>
46
+ safetyRatings?: Array<{ category: string; probability: string }>
46
47
  }>
47
48
  usageMetadata?: {
48
49
  promptTokenCount: number
@@ -52,7 +53,7 @@ interface GeminiResponse {
52
53
  }
53
54
 
54
55
  // ============================================================================
55
- // Google GenAI Client Configuration
56
+ // Google GenAI client configuration
56
57
  // ============================================================================
57
58
 
58
59
  let googleApiKey: string | undefined
@@ -62,9 +63,7 @@ let googleBaseUrl = 'https://generativelanguage.googleapis.com/v1beta'
62
63
  let gatewayUrl: string | undefined
63
64
  let gatewayToken: string | undefined
64
65
 
65
- /**
66
- * Configure the Google GenAI client
67
- */
66
+ /** Configure the Google GenAI client. */
68
67
  export function configureGoogleGenAI(options: {
69
68
  apiKey?: string
70
69
  baseUrl?: string
@@ -79,12 +78,17 @@ export function configureGoogleGenAI(options: {
79
78
  if (options.gatewayToken) gatewayToken = options.gatewayToken
80
79
  }
81
80
 
82
- function getConfig(): { apiKey: string; baseUrl: string; gatewayUrl?: string; gatewayToken?: string } {
83
- // Check for AI Gateway configuration
84
- const gwUrl = gatewayUrl || process.env.AI_GATEWAY_URL
85
- const gwToken = gatewayToken || process.env.AI_GATEWAY_TOKEN
81
+ interface GoogleConfig {
82
+ apiKey: string
83
+ baseUrl: string
84
+ gatewayUrl?: string
85
+ gatewayToken?: string
86
+ }
87
+
88
+ function getConfig(): GoogleConfig {
89
+ const gwUrl = gatewayUrl || process.env['AI_GATEWAY_URL']
90
+ const gwToken = gatewayToken || process.env['AI_GATEWAY_TOKEN']
86
91
 
87
- // If using gateway, we don't need a direct API key
88
92
  if (gwUrl && gwToken) {
89
93
  return {
90
94
  apiKey: '',
@@ -94,268 +98,109 @@ function getConfig(): { apiKey: string; baseUrl: string; gatewayUrl?: string; ga
94
98
  }
95
99
  }
96
100
 
97
- const key = googleApiKey || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY
101
+ const key = googleApiKey || process.env['GOOGLE_API_KEY'] || process.env['GEMINI_API_KEY']
98
102
  if (!key) {
99
103
  throw new Error(
100
104
  'Google API key not configured. Set GOOGLE_API_KEY or GEMINI_API_KEY, or use AI_GATEWAY_URL and AI_GATEWAY_TOKEN'
101
105
  )
102
106
  }
103
- return { apiKey: key, baseUrl: googleBaseUrl, gatewayUrl: undefined, gatewayToken: undefined }
107
+ return { apiKey: key, baseUrl: googleBaseUrl }
104
108
  }
105
109
 
106
110
  // ============================================================================
107
- // In-memory job tracking
111
+ // Local job tracking
108
112
  // ============================================================================
109
113
 
110
- const pendingJobs = new Map<
111
- string,
112
- {
113
- items: BatchItem[]
114
- options: BatchQueueOptions
115
- results: BatchResult[]
116
- status: BatchStatus
117
- createdAt: Date
118
- completedAt?: Date
119
- }
120
- >()
121
-
122
- let jobCounter = 0
114
+ const jobs = new LocalJobStore('google_batch')
123
115
 
124
116
  // ============================================================================
125
- // Google GenAI Batch Adapter
117
+ // Google GenAI batch adapter (BatchProvider port)
126
118
  // ============================================================================
127
119
 
128
- /**
129
- * Google GenAI batch adapter
130
- *
131
- * Note: Google doesn't have a native batch API like OpenAI/Anthropic.
132
- * This adapter implements batch processing via concurrent requests.
133
- * For true async batch processing, consider using Google Cloud Batch
134
- * with Vertex AI.
135
- */
136
120
  const googleAdapter: BatchAdapter = {
137
121
  async submit(items: BatchItem[], options: BatchQueueOptions): Promise<BatchSubmitResult> {
138
- const jobId = `google_batch_${++jobCounter}_${Date.now()}`
139
122
  const model = options.model || 'gemini-2.0-flash'
140
-
141
- // Store job state
142
- pendingJobs.set(jobId, {
143
- items,
144
- options,
145
- results: [],
146
- status: 'pending',
147
- createdAt: new Date(),
148
- })
149
-
150
- // Process requests concurrently
151
- const completion = processGoogleRequestsConcurrently(jobId, items, model, options)
123
+ const { id, state } = jobs.create(items, options)
124
+
125
+ // Drive the job state machine in the background; `waitForCompletion`
126
+ // will poll the in-memory state.
127
+ const completion = (async () => {
128
+ state.status = 'in_progress'
129
+ const results = await processConcurrently(items, (item) => processGoogleItem(item, model), {
130
+ concurrency: 10,
131
+ onWaveComplete: (partial) => {
132
+ state.results = partial
133
+ },
134
+ })
135
+ state.results = results
136
+ state.status = results.every((r) => r.status === 'completed') ? 'completed' : 'failed'
137
+ state.completedAt = new Date()
138
+ return results
139
+ })()
152
140
 
153
141
  const job: BatchJob = {
154
- id: jobId,
142
+ id,
155
143
  provider: 'google',
156
144
  status: 'pending',
157
145
  totalItems: items.length,
158
146
  completedItems: 0,
159
147
  failedItems: 0,
160
- createdAt: new Date(),
161
- webhookUrl: options.webhookUrl,
148
+ createdAt: state.createdAt,
149
+ ...(options.webhookUrl !== undefined && { webhookUrl: options.webhookUrl }),
162
150
  }
163
151
 
164
152
  return { job, completion }
165
153
  },
166
154
 
167
155
  async getStatus(batchId: string): Promise<BatchJob> {
168
- const job = pendingJobs.get(batchId)
169
- if (!job) {
170
- throw new Error(`Batch not found: ${batchId}`)
171
- }
172
-
173
- const completedItems = job.results.filter((r) => r.status === 'completed').length
174
- const failedItems = job.results.filter((r) => r.status === 'failed').length
175
-
176
- return {
177
- id: batchId,
178
- provider: 'google',
179
- status: job.status,
180
- totalItems: job.items.length,
181
- completedItems,
182
- failedItems,
183
- createdAt: job.createdAt,
184
- completedAt: job.completedAt,
185
- }
156
+ return jobs.snapshot(batchId, 'google')
186
157
  },
187
158
 
188
159
  async cancel(batchId: string): Promise<void> {
189
- const job = pendingJobs.get(batchId)
190
- if (job) {
191
- job.status = 'cancelled'
160
+ if (jobs.has(batchId)) {
161
+ jobs.get(batchId).status = 'cancelled'
192
162
  }
193
163
  },
194
164
 
195
165
  async getResults(batchId: string): Promise<BatchResult[]> {
196
- const job = pendingJobs.get(batchId)
197
- if (!job) {
198
- throw new Error(`Batch not found: ${batchId}`)
199
- }
200
- return job.results
166
+ return jobs.get(batchId).results
201
167
  },
202
168
 
203
169
  async waitForCompletion(batchId: string, pollInterval = 1000): Promise<BatchResult[]> {
204
- const job = pendingJobs.get(batchId)
205
- if (!job) {
206
- throw new Error(`Batch not found: ${batchId}`)
207
- }
208
-
209
- while (job.status !== 'completed' && job.status !== 'failed' && job.status !== 'cancelled') {
210
- await new Promise((resolve) => setTimeout(resolve, pollInterval))
211
- }
212
-
213
- return job.results
170
+ return jobs.waitForCompletion(batchId, pollInterval)
214
171
  },
215
172
  }
216
173
 
217
174
  // ============================================================================
218
- // Google GenAI Flex Adapter
175
+ // Google GenAI flex adapter (FlexAdapter port)
219
176
  // ============================================================================
220
177
 
221
- /**
222
- * Google GenAI Flex Adapter
223
- *
224
- * Implements concurrent processing for medium-sized batches.
225
- * Uses the Gemini API for fast turnaround.
226
- */
227
178
  const googleFlexAdapter: FlexAdapter = {
228
179
  async submitFlex(items: BatchItem[], options: { model?: string }): Promise<BatchResult[]> {
229
180
  const model = options.model || 'gemini-2.0-flash'
230
- const CONCURRENCY = 10
231
-
232
- const results: BatchResult[] = []
233
-
234
- // Process items concurrently
235
- for (let i = 0; i < items.length; i += CONCURRENCY) {
236
- const batch = items.slice(i, i + CONCURRENCY)
237
-
238
- const batchResults = await Promise.all(
239
- batch.map(async (item) => {
240
- try {
241
- return await processGoogleItem(item, model)
242
- } catch (error) {
243
- return {
244
- id: item.id,
245
- customId: item.id,
246
- status: 'failed' as const,
247
- error: error instanceof Error ? error.message : 'Unknown error',
248
- }
249
- }
250
- })
251
- )
252
-
253
- results.push(...batchResults)
254
- }
255
-
256
- return results
181
+ return processConcurrently(items, (item) => processGoogleItem(item, model), {
182
+ concurrency: 10,
183
+ })
257
184
  },
258
185
  }
259
186
 
260
187
  // ============================================================================
261
- // Processing
188
+ // Per-item processing
262
189
  // ============================================================================
263
190
 
264
- async function processGoogleRequestsConcurrently(
265
- jobId: string,
266
- items: BatchItem[],
267
- model: string,
268
- options: BatchQueueOptions
269
- ): Promise<BatchResult[]> {
270
- const job = pendingJobs.get(jobId)
271
- if (!job) {
272
- throw new Error(`Job not found: ${jobId}`)
273
- }
274
-
275
- job.status = 'in_progress'
276
-
277
- const CONCURRENCY = 10
278
- const results: BatchResult[] = []
279
-
280
- for (let i = 0; i < items.length; i += CONCURRENCY) {
281
- const batch = items.slice(i, i + CONCURRENCY)
282
-
283
- const batchResults = await Promise.all(
284
- batch.map(async (item) => {
285
- try {
286
- return await processGoogleItem(item, model)
287
- } catch (error) {
288
- return {
289
- id: item.id,
290
- customId: item.id,
291
- status: 'failed' as const,
292
- error: error instanceof Error ? error.message : 'Unknown error',
293
- }
294
- }
295
- })
296
- )
297
-
298
- results.push(...batchResults)
299
- job.results = results
300
- }
301
-
302
- job.status = results.every((r) => r.status === 'completed') ? 'completed' : 'failed'
303
- job.completedAt = new Date()
304
-
305
- return results
306
- }
307
-
308
191
  async function processGoogleItem(item: BatchItem, model: string): Promise<BatchResult> {
309
192
  const config = getConfig()
310
-
311
- // Check if using AI Gateway
312
193
  if (config.gatewayUrl && config.gatewayToken) {
313
194
  return processGoogleItemViaGateway(item, config, model)
314
195
  }
315
196
 
316
- // Build the model name (add models/ prefix if not present)
317
197
  const modelName = model.startsWith('models/') ? model : `models/${model}`
318
-
319
198
  const url = `${config.baseUrl}/${modelName}:generateContent?key=${config.apiKey}`
320
199
 
321
- // Build messages
322
- const contents: GeminiMessage[] = []
323
-
324
- // Add system instruction as a user message if provided (Gemini handles this differently)
325
- if (item.options?.system) {
326
- contents.push({
327
- role: 'user',
328
- parts: [{ text: `System instruction: ${item.options.system}\n\nUser request: ${item.prompt}` }],
329
- })
330
- } else {
331
- contents.push({
332
- role: 'user',
333
- parts: [{ text: item.prompt }],
334
- })
335
- }
336
-
337
- const body: Record<string, unknown> = {
338
- contents,
339
- generationConfig: {
340
- maxOutputTokens: item.options?.maxTokens || 8192,
341
- temperature: item.options?.temperature,
342
- },
343
- }
344
-
345
- // Add JSON mode if schema is provided
346
- if (item.schema) {
347
- body.generationConfig = {
348
- ...(body.generationConfig as object),
349
- responseMimeType: 'application/json',
350
- }
351
- }
352
-
353
200
  const response = await fetch(url, {
354
201
  method: 'POST',
355
- headers: {
356
- 'Content-Type': 'application/json',
357
- },
358
- body: JSON.stringify(body),
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify(buildGeminiBody(item)),
359
204
  })
360
205
 
361
206
  if (!response.ok) {
@@ -363,88 +208,28 @@ async function processGoogleItem(item: BatchItem, model: string): Promise<BatchR
363
208
  throw new Error(`Google GenAI API error: ${response.status} ${error}`)
364
209
  }
365
210
 
366
- const data = (await response.json()) as GeminiResponse
367
-
368
- // Extract content
369
- const content = data.candidates?.[0]?.content?.parts?.[0]?.text
370
-
371
- let result: unknown = content
372
-
373
- // Try to parse JSON if schema was provided or content looks like JSON
374
- if (content && (item.schema || content.trim().startsWith('{') || content.trim().startsWith('['))) {
375
- try {
376
- result = JSON.parse(content)
377
- } catch {
378
- // Keep as string
379
- }
380
- }
381
-
382
- return {
383
- id: item.id,
384
- customId: item.id,
385
- status: 'completed',
386
- result,
387
- usage: data.usageMetadata
388
- ? {
389
- promptTokens: data.usageMetadata.promptTokenCount,
390
- completionTokens: data.usageMetadata.candidatesTokenCount,
391
- totalTokens: data.usageMetadata.totalTokenCount,
392
- }
393
- : undefined,
394
- }
211
+ return parseGeminiResponse(item, (await response.json()) as GeminiResponse)
395
212
  }
396
213
 
397
214
  /**
398
- * Process a Google GenAI item via Cloudflare AI Gateway
215
+ * Process a Google GenAI item via Cloudflare AI Gateway.
399
216
  * Gateway URL format: {gateway_url}/google-ai-studio/v1beta/models/{model}:generateContent
400
217
  */
401
218
  async function processGoogleItemViaGateway(
402
219
  item: BatchItem,
403
- config: ReturnType<typeof getConfig>,
220
+ config: GoogleConfig,
404
221
  model: string
405
222
  ): Promise<BatchResult> {
406
- // AI Gateway URL for Google AI Studio
407
- // Format: {gateway_url}/google-ai-studio/v1beta/models/{model}:generateContent
408
223
  const modelName = model.startsWith('models/') ? model.replace('models/', '') : model
409
224
  const url = `${config.gatewayUrl}/google-ai-studio/v1beta/models/${modelName}:generateContent`
410
225
 
411
- // Build messages
412
- const contents: GeminiMessage[] = []
413
-
414
- if (item.options?.system) {
415
- contents.push({
416
- role: 'user',
417
- parts: [{ text: `System instruction: ${item.options.system}\n\nUser request: ${item.prompt}` }],
418
- })
419
- } else {
420
- contents.push({
421
- role: 'user',
422
- parts: [{ text: item.prompt }],
423
- })
424
- }
425
-
426
- const body: Record<string, unknown> = {
427
- contents,
428
- generationConfig: {
429
- maxOutputTokens: item.options?.maxTokens || 8192,
430
- temperature: item.options?.temperature,
431
- },
432
- }
433
-
434
- if (item.schema) {
435
- body.generationConfig = {
436
- ...(body.generationConfig as object),
437
- responseMimeType: 'application/json',
438
- }
439
- }
440
-
441
226
  const response = await fetch(url, {
442
227
  method: 'POST',
443
228
  headers: {
444
229
  'cf-aig-authorization': `Bearer ${config.gatewayToken}`,
445
230
  'Content-Type': 'application/json',
446
231
  },
447
- body: JSON.stringify(body),
232
+ body: JSON.stringify(buildGeminiBody(item)),
448
233
  })
449
234
 
450
235
  if (!response.ok) {
@@ -452,37 +237,46 @@ async function processGoogleItemViaGateway(
452
237
  throw new Error(`Google GenAI via Gateway error: ${response.status} ${error}`)
453
238
  }
454
239
 
455
- const data = (await response.json()) as GeminiResponse
240
+ return parseGeminiResponse(item, (await response.json()) as GeminiResponse)
241
+ }
456
242
 
457
- const content = data.candidates?.[0]?.content?.parts?.[0]?.text
243
+ function buildGeminiBody(item: BatchItem): Record<string, unknown> {
244
+ // Gemini handles system instructions as part of the user message.
245
+ const userText = item.options?.system
246
+ ? `System instruction: ${item.options.system}\n\nUser request: ${item.prompt}`
247
+ : item.prompt
458
248
 
459
- let result: unknown = content
249
+ const contents: GeminiMessage[] = [{ role: 'user', parts: [{ text: userText }] }]
460
250
 
461
- if (content && (item.schema || content.trim().startsWith('{') || content.trim().startsWith('['))) {
462
- try {
463
- result = JSON.parse(content)
464
- } catch {
465
- // Keep as string
466
- }
251
+ const generationConfig: Record<string, unknown> = {
252
+ maxOutputTokens: item.options?.maxTokens || 8192,
253
+ ...(item.options?.temperature !== undefined && { temperature: item.options.temperature }),
254
+ ...(item.schema && { responseMimeType: 'application/json' }),
467
255
  }
468
256
 
257
+ return { contents, generationConfig }
258
+ }
259
+
260
+ function parseGeminiResponse(item: BatchItem, data: GeminiResponse): BatchResult {
261
+ const content = data.candidates?.[0]?.content?.parts?.[0]?.text
262
+
469
263
  return {
470
264
  id: item.id,
471
265
  customId: item.id,
472
266
  status: 'completed',
473
- result,
474
- usage: data.usageMetadata
475
- ? {
476
- promptTokens: data.usageMetadata.promptTokenCount,
477
- completionTokens: data.usageMetadata.candidatesTokenCount,
478
- totalTokens: data.usageMetadata.totalTokenCount,
479
- }
480
- : undefined,
267
+ result: tryParseJson(content, !!item.schema),
268
+ ...(data.usageMetadata && {
269
+ usage: {
270
+ promptTokens: data.usageMetadata.promptTokenCount,
271
+ completionTokens: data.usageMetadata.candidatesTokenCount,
272
+ totalTokens: data.usageMetadata.totalTokenCount,
273
+ },
274
+ }),
481
275
  }
482
276
  }
483
277
 
484
278
  // ============================================================================
485
- // Register Adapters
279
+ // Register adapters
486
280
  // ============================================================================
487
281
 
488
282
  registerBatchAdapter('google', googleAdapter)
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Batch Adapters Index
3
3
  *
4
- * Import specific adapters to register them:
4
+ * The `BatchProvider` port lives in `./provider.js`. Each adapter file is a
5
+ * small implementation of that port for one provider. Import a specific
6
+ * adapter to register it:
5
7
  *
6
8
  * @example
7
9
  * ```ts
@@ -24,6 +26,7 @@
24
26
  * @packageDocumentation
25
27
  */
26
28
 
29
+ export * from './provider.js'
27
30
  export * from './openai.js'
28
31
  export * from './anthropic.js'
29
32
  export * from './cloudflare.js'
@@ -15,7 +15,7 @@ import {
15
15
  type BatchQueueOptions,
16
16
  type BatchResult,
17
17
  type BatchSubmitResult,
18
- } from '../batch-queue.js'
18
+ } from './provider.js'
19
19
  import { generateObject, generateText } from '../generate.js'
20
20
 
21
21
  // ============================================================================
@@ -105,7 +105,7 @@ const memoryAdapter: BatchAdapter = {
105
105
  completedItems: 0,
106
106
  failedItems: 0,
107
107
  createdAt: batch.createdAt,
108
- webhookUrl: options.webhookUrl,
108
+ ...(options.webhookUrl !== undefined && { webhookUrl: options.webhookUrl }),
109
109
  }
110
110
 
111
111
  return { job, completion }
@@ -123,12 +123,17 @@ const memoryAdapter: BatchAdapter = {
123
123
  return {
124
124
  id: batch.id,
125
125
  provider: 'openai',
126
- status: batch.status === 'completed' ? 'completed' : batch.status === 'failed' ? 'failed' : 'in_progress',
126
+ status:
127
+ batch.status === 'completed'
128
+ ? 'completed'
129
+ : batch.status === 'failed'
130
+ ? 'failed'
131
+ : 'in_progress',
127
132
  totalItems: batch.items.length,
128
133
  completedItems,
129
134
  failedItems,
130
135
  createdAt: batch.createdAt,
131
- completedAt: batch.completedAt,
136
+ ...(batch.completedAt && { completedAt: batch.completedAt }),
132
137
  }
133
138
  },
134
139
 
@@ -195,9 +200,9 @@ async function processMemoryBatch(batch: MemoryBatch): Promise<BatchResult[]> {
195
200
  model: batch.options.model || 'sonnet',
196
201
  schema: item.schema,
197
202
  prompt: item.prompt,
198
- system: item.options?.system,
199
- temperature: item.options?.temperature,
200
- maxTokens: item.options?.maxTokens,
203
+ ...(item.options?.system !== undefined && { system: item.options.system }),
204
+ ...(item.options?.temperature !== undefined && { temperature: item.options.temperature }),
205
+ ...(item.options?.maxTokens !== undefined && { maxTokens: item.options.maxTokens }),
201
206
  })
202
207
  result = response.object
203
208
  } else {
@@ -205,9 +210,9 @@ async function processMemoryBatch(batch: MemoryBatch): Promise<BatchResult[]> {
205
210
  const response = await generateText({
206
211
  model: batch.options.model || 'sonnet',
207
212
  prompt: item.prompt,
208
- system: item.options?.system,
209
- temperature: item.options?.temperature,
210
- maxTokens: item.options?.maxTokens,
213
+ ...(item.options?.system !== undefined && { system: item.options.system }),
214
+ ...(item.options?.temperature !== undefined && { temperature: item.options.temperature }),
215
+ ...(item.options?.maxTokens !== undefined && { maxTokens: item.options.maxTokens }),
211
216
  })
212
217
  result = response.text
213
218
  }