digital-workers 2.1.3 → 2.4.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 (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +33 -21
  6. package/dist/actions.js.map +1 -1
  7. package/dist/agent-comms.d.ts.map +1 -1
  8. package/dist/agent-comms.js +36 -25
  9. package/dist/agent-comms.js.map +1 -1
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.js +3 -3
  23. package/dist/capability-tiers.js.map +1 -1
  24. package/dist/cascade-context.d.ts +28 -28
  25. package/dist/client.d.ts +162 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +64 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/decide.d.ts +42 -6
  30. package/dist/decide.d.ts.map +1 -1
  31. package/dist/decide.js +54 -11
  32. package/dist/decide.js.map +1 -1
  33. package/dist/do.d.ts +36 -7
  34. package/dist/do.d.ts.map +1 -1
  35. package/dist/do.js +82 -39
  36. package/dist/do.js.map +1 -1
  37. package/dist/error-escalation.d.ts.map +1 -1
  38. package/dist/error-escalation.js +38 -38
  39. package/dist/error-escalation.js.map +1 -1
  40. package/dist/generate.d.ts +48 -7
  41. package/dist/generate.d.ts.map +1 -1
  42. package/dist/generate.js +49 -8
  43. package/dist/generate.js.map +1 -1
  44. package/dist/goals.d.ts +10 -9
  45. package/dist/goals.d.ts.map +1 -1
  46. package/dist/goals.js +30 -24
  47. package/dist/goals.js.map +1 -1
  48. package/dist/image.d.ts +189 -0
  49. package/dist/image.d.ts.map +1 -0
  50. package/dist/image.js +528 -0
  51. package/dist/image.js.map +1 -0
  52. package/dist/index.d.ts +49 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +58 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/is.d.ts +45 -10
  57. package/dist/is.d.ts.map +1 -1
  58. package/dist/is.js +56 -21
  59. package/dist/is.js.map +1 -1
  60. package/dist/kpis.d.ts +24 -15
  61. package/dist/kpis.d.ts.map +1 -1
  62. package/dist/kpis.js +16 -14
  63. package/dist/kpis.js.map +1 -1
  64. package/dist/load-balancing.d.ts.map +1 -1
  65. package/dist/load-balancing.js +124 -38
  66. package/dist/load-balancing.js.map +1 -1
  67. package/dist/logger.d.ts +76 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +39 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/notify.d.ts +38 -9
  72. package/dist/notify.d.ts.map +1 -1
  73. package/dist/notify.js +72 -17
  74. package/dist/notify.js.map +1 -1
  75. package/dist/role.d.ts +5 -4
  76. package/dist/role.d.ts.map +1 -1
  77. package/dist/role.js +13 -10
  78. package/dist/role.js.map +1 -1
  79. package/dist/runtime.d.ts +310 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +510 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/team.d.ts +11 -6
  84. package/dist/team.d.ts.map +1 -1
  85. package/dist/team.js +22 -15
  86. package/dist/team.js.map +1 -1
  87. package/dist/transports/email.d.ts +318 -0
  88. package/dist/transports/email.d.ts.map +1 -0
  89. package/dist/transports/email.js +779 -0
  90. package/dist/transports/email.js.map +1 -0
  91. package/dist/transports/slack.d.ts +515 -0
  92. package/dist/transports/slack.d.ts.map +1 -0
  93. package/dist/transports/slack.js +844 -0
  94. package/dist/transports/slack.js.map +1 -0
  95. package/dist/transports.d.ts.map +1 -1
  96. package/dist/transports.js +44 -25
  97. package/dist/transports.js.map +1 -1
  98. package/dist/types.d.ts +141 -19
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +5 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/id.d.ts +19 -0
  103. package/dist/utils/id.d.ts.map +1 -0
  104. package/dist/utils/id.js +21 -0
  105. package/dist/utils/id.js.map +1 -0
  106. package/dist/video.d.ts +203 -0
  107. package/dist/video.d.ts.map +1 -0
  108. package/dist/video.js +528 -0
  109. package/dist/video.js.map +1 -0
  110. package/dist/worker.d.ts +343 -0
  111. package/dist/worker.d.ts.map +1 -0
  112. package/dist/worker.js +698 -0
  113. package/dist/worker.js.map +1 -0
  114. package/package.json +32 -14
  115. package/src/actions.ts +39 -30
  116. package/src/agent-comms.ts +54 -92
  117. package/src/approve.ts +91 -20
  118. package/src/ask.ts +99 -25
  119. package/src/browse.ts +627 -0
  120. package/src/capability-tiers.ts +5 -5
  121. package/src/client.ts +221 -0
  122. package/src/decide.ts +81 -35
  123. package/src/do.ts +98 -52
  124. package/src/error-escalation.ts +55 -67
  125. package/src/generate.ts +52 -18
  126. package/src/goals.ts +36 -27
  127. package/src/image.ts +816 -0
  128. package/src/index.ts +187 -2
  129. package/src/is.ts +59 -25
  130. package/src/kpis.ts +41 -36
  131. package/src/load-balancing.ts +132 -46
  132. package/src/logger.ts +93 -0
  133. package/src/notify.ts +78 -17
  134. package/src/role.ts +30 -20
  135. package/src/runtime.ts +796 -0
  136. package/src/team.ts +24 -19
  137. package/src/transports/email.ts +1160 -0
  138. package/src/transports/slack.ts +1320 -0
  139. package/src/transports.ts +58 -43
  140. package/src/types.ts +174 -46
  141. package/src/utils/id.ts +21 -0
  142. package/src/video.ts +906 -0
  143. package/src/worker.ts +1007 -0
  144. package/test/approve.test.ts +305 -0
  145. package/test/ask.test.ts +274 -0
  146. package/test/browse.test.ts +361 -0
  147. package/test/decide.test.ts +252 -0
  148. package/test/do.test.ts +144 -0
  149. package/test/error-logging.test.ts +357 -0
  150. package/test/generate.test.ts +319 -0
  151. package/test/image.test.ts +398 -0
  152. package/test/is.test.ts +287 -0
  153. package/test/load-balancing-safety.test.ts +404 -0
  154. package/test/notify.test.ts +434 -0
  155. package/test/primitives.test.ts +320 -0
  156. package/test/runtime-integration.test.ts +892 -0
  157. package/test/transports/crypto.test.ts +230 -0
  158. package/test/transports/email.test.ts +866 -0
  159. package/test/transports/id-generation.test.ts +91 -0
  160. package/test/transports/slack.test.ts +760 -0
  161. package/test/type-safety.test.ts +834 -0
  162. package/test/types.test.ts +60 -2
  163. package/test/video.test.ts +530 -0
  164. package/test/worker.test.ts +1433 -0
  165. package/tsconfig.json +4 -1
  166. package/vitest.config.ts +42 -0
  167. package/wrangler.jsonc +36 -0
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
package/src/image.ts ADDED
@@ -0,0 +1,816 @@
1
+ /**
2
+ * Image generation functionality for digital workers
3
+ *
4
+ * This module provides image generation primitives within a worker context,
5
+ * with rich metadata about the generation process.
6
+ *
7
+ * - `image()` - Generates images with full metadata (model, size, style)
8
+ * - `image.variations()` - Creates variations of an existing image
9
+ * - `image.edit()` - Edits an image with a text prompt and optional mask
10
+ * - `image.upscale()` - Upscales an image to higher resolution
11
+ * - `image.style()` - Creates a curried function for a specific style
12
+ *
13
+ * The key difference from direct API calls is context and metadata:
14
+ * - digital-workers returns `ImageResult` with content + metadata
15
+ * - Direct API calls return just the generated image
16
+ *
17
+ * @module
18
+ */
19
+
20
+ /**
21
+ * Available image style presets
22
+ */
23
+ export type ImageStyle =
24
+ | 'realistic'
25
+ | 'artistic'
26
+ | 'cartoon'
27
+ | 'abstract'
28
+ | 'photographic'
29
+ | 'digital-art'
30
+ | 'cinematic'
31
+
32
+ /**
33
+ * Available image sizes
34
+ */
35
+ export type ImageSize = '256x256' | '512x512' | '1024x1024' | '1024x1792' | '1792x1024'
36
+
37
+ /**
38
+ * Available image formats
39
+ */
40
+ export type ImageFormat = 'png' | 'jpeg' | 'webp' | 'b64_json' | 'url'
41
+
42
+ /**
43
+ * Options for image generation
44
+ */
45
+ export interface ImageOptions {
46
+ /** The prompt describing the image to generate */
47
+ prompt: string
48
+ /** The style preset to apply */
49
+ style?: ImageStyle
50
+ /** The size of the generated image */
51
+ size?: ImageSize
52
+ /** The model to use for generation */
53
+ model?: string
54
+ /** Number of images to generate (1-10) */
55
+ n?: number
56
+ /** Output format */
57
+ format?: ImageFormat
58
+ /** Quality setting */
59
+ quality?: 'standard' | 'hd'
60
+ /** Negative prompt - what to avoid in the image */
61
+ negativePrompt?: string
62
+ /** Seed for reproducible generation */
63
+ seed?: number
64
+ /** Additional model-specific parameters */
65
+ parameters?: Record<string, unknown>
66
+ }
67
+
68
+ /**
69
+ * Result of image generation
70
+ */
71
+ export interface ImageResult {
72
+ /** URL or base64 data of the generated image */
73
+ url: string
74
+ /** The original prompt */
75
+ prompt: string
76
+ /** The revised/enhanced prompt if the model modified it */
77
+ revisedPrompt?: string
78
+ /** Generation metadata */
79
+ metadata: {
80
+ /** Model used for generation */
81
+ model: string
82
+ /** Size of the generated image */
83
+ size: string
84
+ /** Style applied */
85
+ style?: string
86
+ /** Generation duration in milliseconds */
87
+ duration?: number
88
+ /** Seed used for generation */
89
+ seed?: number
90
+ /** Format of the output */
91
+ format?: ImageFormat
92
+ /** Quality setting used */
93
+ quality?: string
94
+ /** Provider-specific metadata */
95
+ provider?: Record<string, unknown>
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Options for image variations
101
+ */
102
+ export interface VariationOptions {
103
+ /** Number of variations to generate */
104
+ count?: number
105
+ /** Size of the generated variations */
106
+ size?: ImageSize
107
+ /** Model to use for generation */
108
+ model?: string
109
+ /** Output format */
110
+ format?: ImageFormat
111
+ }
112
+
113
+ /**
114
+ * Options for image editing
115
+ */
116
+ export interface EditOptions {
117
+ /** The prompt describing the edit */
118
+ prompt: string
119
+ /** URL or base64 of the mask image (transparent areas will be edited) */
120
+ mask?: string
121
+ /** Size of the output image */
122
+ size?: ImageSize
123
+ /** Model to use for editing */
124
+ model?: string
125
+ /** Number of edits to generate */
126
+ n?: number
127
+ /** Output format */
128
+ format?: ImageFormat
129
+ }
130
+
131
+ /**
132
+ * Options for image upscaling
133
+ */
134
+ export interface UpscaleOptions {
135
+ /** Scale factor (2, 4, etc.) */
136
+ scale?: number
137
+ /** Model to use for upscaling */
138
+ model?: string
139
+ /** Output format */
140
+ format?: ImageFormat
141
+ /** Denoise level (0-1) */
142
+ denoise?: number
143
+ }
144
+
145
+ /**
146
+ * Result of image upscaling
147
+ */
148
+ export interface UpscaleResult extends ImageResult {
149
+ /** Original image dimensions */
150
+ originalSize: {
151
+ width: number
152
+ height: number
153
+ }
154
+ /** Upscaled image dimensions */
155
+ upscaledSize: {
156
+ width: number
157
+ height: number
158
+ }
159
+ /** Scale factor used */
160
+ scaleFactor: number
161
+ }
162
+
163
+ /**
164
+ * Style preset configurations
165
+ */
166
+ const STYLE_PROMPTS: Record<ImageStyle, string> = {
167
+ realistic: 'photorealistic, highly detailed, natural lighting, 8k resolution',
168
+ artistic: 'artistic interpretation, painterly style, creative composition',
169
+ cartoon: 'cartoon style, cel-shaded, vibrant colors, animated look',
170
+ abstract: 'abstract art, non-representational, geometric shapes, modern art',
171
+ photographic: 'professional photography, DSLR quality, sharp focus, studio lighting',
172
+ 'digital-art': 'digital art, concept art, detailed illustration, artstation trending',
173
+ cinematic: 'cinematic composition, dramatic lighting, movie scene, film grain',
174
+ }
175
+
176
+ /**
177
+ * Default configuration
178
+ */
179
+ const DEFAULTS = {
180
+ model: 'dall-e-3',
181
+ size: '1024x1024' as ImageSize,
182
+ format: 'url' as ImageFormat,
183
+ quality: 'standard' as const,
184
+ n: 1,
185
+ }
186
+
187
+ // Declare process for environments where it exists (Node.js)
188
+ declare const process: { env?: Record<string, string | undefined> } | undefined
189
+
190
+ /**
191
+ * Get environment variable safely (works in both Node.js and Workers)
192
+ */
193
+ function getEnv(key: string): string | undefined {
194
+ if (typeof process !== 'undefined' && process?.env) {
195
+ return process.env[key]
196
+ }
197
+ return undefined
198
+ }
199
+
200
+ /**
201
+ * Get the image generation endpoint based on model
202
+ */
203
+ function getImageEndpoint(model: string): string {
204
+ // Route to appropriate worker based on model
205
+ const baseUrl =
206
+ getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
207
+
208
+ if (model.startsWith('dall-e') || model.startsWith('openai/')) {
209
+ return `${baseUrl}/openai/images/generations`
210
+ }
211
+
212
+ if (model.startsWith('stable-diffusion') || model.startsWith('stability/')) {
213
+ return `${baseUrl}/stability/images/generations`
214
+ }
215
+
216
+ if (model.startsWith('flux') || model.startsWith('black-forest/')) {
217
+ return `${baseUrl}/flux/images/generations`
218
+ }
219
+
220
+ if (model.startsWith('midjourney/')) {
221
+ return `${baseUrl}/midjourney/images/generations`
222
+ }
223
+
224
+ // Default to generic endpoint
225
+ return `${baseUrl}/images/generations`
226
+ }
227
+
228
+ /**
229
+ * Get authorization headers
230
+ */
231
+ function getAuthHeaders(): Record<string, string> {
232
+ const headers: Record<string, string> = {}
233
+
234
+ const openaiKey = getEnv('OPENAI_API_KEY')
235
+ if (openaiKey) {
236
+ headers['Authorization'] = `Bearer ${openaiKey}`
237
+ }
238
+
239
+ const gatewayToken = getEnv('AI_GATEWAY_TOKEN')
240
+ if (gatewayToken) {
241
+ headers['X-Gateway-Token'] = gatewayToken
242
+ }
243
+
244
+ return headers
245
+ }
246
+
247
+ /**
248
+ * Enhance prompt with style modifiers
249
+ */
250
+ function enhancePrompt(prompt: string, style?: ImageStyle): string {
251
+ if (!style || !STYLE_PROMPTS[style]) {
252
+ return prompt
253
+ }
254
+ return `${prompt}, ${STYLE_PROMPTS[style]}`
255
+ }
256
+
257
+ /**
258
+ * Convert base64 data URL to Blob
259
+ */
260
+ function base64ToBlob(dataUrl: string): Blob {
261
+ const base64Data = dataUrl.split(',')[1]
262
+ if (!base64Data) {
263
+ throw new Error('Invalid data URL format')
264
+ }
265
+ const binaryData = atob(base64Data)
266
+ const bytes = new Uint8Array(binaryData.length)
267
+ for (let i = 0; i < binaryData.length; i++) {
268
+ bytes[i] = binaryData.charCodeAt(i)
269
+ }
270
+ return new Blob([bytes], { type: 'image/png' })
271
+ }
272
+
273
+ /**
274
+ * Generate an image from a text prompt
275
+ *
276
+ * @param prompt - The text prompt describing the image to generate
277
+ * @param options - Generation options (style, size, model, etc.)
278
+ * @returns Promise resolving to ImageResult with URL and metadata
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * // Generate a simple image
283
+ * const result = await image('A sunset over mountains')
284
+ * console.log(result.url) // URL to the generated image
285
+ * console.log(result.metadata.model) // Model used
286
+ * ```
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * // Generate with options
291
+ * const result = await image('A portrait of a robot', {
292
+ * style: 'cinematic',
293
+ * size: '1024x1024',
294
+ * quality: 'hd',
295
+ * })
296
+ * ```
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * // Generate multiple images
301
+ * const result = await image('Abstract patterns', {
302
+ * style: 'abstract',
303
+ * n: 4,
304
+ * })
305
+ * ```
306
+ */
307
+ export async function image(
308
+ prompt: string,
309
+ options: Partial<ImageOptions> = {}
310
+ ): Promise<ImageResult> {
311
+ const startTime = Date.now()
312
+
313
+ const {
314
+ style,
315
+ size = DEFAULTS.size,
316
+ model = DEFAULTS.model,
317
+ n = DEFAULTS.n,
318
+ format = DEFAULTS.format,
319
+ quality = DEFAULTS.quality,
320
+ negativePrompt,
321
+ seed,
322
+ parameters = {},
323
+ } = options
324
+
325
+ const enhancedPrompt = enhancePrompt(prompt, style)
326
+ const endpoint = getImageEndpoint(model)
327
+
328
+ // Build the request payload
329
+ const payload: Record<string, unknown> = {
330
+ prompt: enhancedPrompt,
331
+ size,
332
+ n,
333
+ response_format: format === 'url' ? 'url' : format === 'b64_json' ? 'b64_json' : 'url',
334
+ ...parameters,
335
+ }
336
+
337
+ // Add optional parameters using bracket notation for index signatures
338
+ if (quality && model.includes('dall-e-3')) {
339
+ payload['quality'] = quality
340
+ }
341
+
342
+ if (negativePrompt) {
343
+ payload['negative_prompt'] = negativePrompt
344
+ }
345
+
346
+ if (seed !== undefined) {
347
+ payload['seed'] = seed
348
+ }
349
+
350
+ if (model) {
351
+ payload['model'] = model
352
+ }
353
+
354
+ // Make the API request
355
+ const response = await fetch(endpoint, {
356
+ method: 'POST',
357
+ headers: {
358
+ 'Content-Type': 'application/json',
359
+ ...getAuthHeaders(),
360
+ },
361
+ body: JSON.stringify(payload),
362
+ })
363
+
364
+ if (!response.ok) {
365
+ const error = await response.text()
366
+ throw new Error(`Image generation failed: ${response.status} - ${error}`)
367
+ }
368
+
369
+ const apiResponse = (await response.json()) as {
370
+ data?: Array<{
371
+ url?: string
372
+ b64_json?: string
373
+ revised_prompt?: string
374
+ }>
375
+ created?: number
376
+ }
377
+
378
+ const imageData = apiResponse.data?.[0]
379
+
380
+ if (!imageData) {
381
+ throw new Error('No image data returned from API')
382
+ }
383
+
384
+ const imageUrl =
385
+ imageData.url || (imageData.b64_json ? `data:image/png;base64,${imageData.b64_json}` : '')
386
+
387
+ if (!imageUrl) {
388
+ throw new Error('No image URL or data returned from API')
389
+ }
390
+
391
+ // Build metadata, only including defined values
392
+ const metadata: ImageResult['metadata'] = {
393
+ model,
394
+ size,
395
+ duration: Date.now() - startTime,
396
+ format,
397
+ quality,
398
+ }
399
+
400
+ if (style) {
401
+ metadata.style = style
402
+ }
403
+
404
+ if (seed !== undefined) {
405
+ metadata.seed = seed
406
+ }
407
+
408
+ const imageResult: ImageResult = {
409
+ url: imageUrl,
410
+ prompt,
411
+ metadata,
412
+ }
413
+
414
+ // Only add revisedPrompt if it's defined (exactOptionalPropertyTypes)
415
+ if (imageData.revised_prompt) {
416
+ imageResult.revisedPrompt = imageData.revised_prompt
417
+ }
418
+
419
+ return imageResult
420
+ }
421
+
422
+ /**
423
+ * Generate variations of an existing image
424
+ *
425
+ * @param imageUrl - URL or base64 of the source image
426
+ * @param options - Variation options
427
+ * @returns Promise resolving to array of ImageResults
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * const variations = await image.variations('https://example.com/image.png', {
432
+ * count: 3,
433
+ * size: '1024x1024',
434
+ * })
435
+ *
436
+ * variations.forEach((v, i) => {
437
+ * console.log(`Variation ${i + 1}: ${v.url}`)
438
+ * })
439
+ * ```
440
+ */
441
+ image.variations = async function variations(
442
+ imageUrl: string,
443
+ options: VariationOptions = {}
444
+ ): Promise<ImageResult[]> {
445
+ const startTime = Date.now()
446
+
447
+ const {
448
+ count = 3,
449
+ size = DEFAULTS.size,
450
+ model = 'dall-e-2', // DALL-E 2 supports variations
451
+ format = DEFAULTS.format,
452
+ } = options
453
+
454
+ const baseUrl =
455
+ getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
456
+ const endpoint = `${baseUrl}/openai/images/variations`
457
+
458
+ // Build form data for image upload
459
+ const formData = new FormData()
460
+
461
+ // If it's a URL, we need to fetch the image first
462
+ if (imageUrl.startsWith('http')) {
463
+ const imageResponse = await fetch(imageUrl)
464
+ const imageBlob = await imageResponse.blob()
465
+ formData.append('image', imageBlob, 'image.png')
466
+ } else if (imageUrl.startsWith('data:')) {
467
+ // Handle base64 data URL
468
+ const blob = base64ToBlob(imageUrl)
469
+ formData.append('image', blob, 'image.png')
470
+ } else {
471
+ throw new Error('Invalid image URL format. Must be an HTTP URL or data URL.')
472
+ }
473
+
474
+ formData.append('n', String(count))
475
+ formData.append('size', size)
476
+ formData.append('response_format', format === 'b64_json' ? 'b64_json' : 'url')
477
+ if (model) {
478
+ formData.append('model', model)
479
+ }
480
+
481
+ const response = await fetch(endpoint, {
482
+ method: 'POST',
483
+ headers: getAuthHeaders(),
484
+ body: formData,
485
+ })
486
+
487
+ if (!response.ok) {
488
+ const error = await response.text()
489
+ throw new Error(`Image variations failed: ${response.status} - ${error}`)
490
+ }
491
+
492
+ const apiResponse = (await response.json()) as {
493
+ data?: Array<{
494
+ url?: string
495
+ b64_json?: string
496
+ }>
497
+ }
498
+
499
+ if (!apiResponse.data || apiResponse.data.length === 0) {
500
+ throw new Error('No variations returned from API')
501
+ }
502
+
503
+ const duration = Date.now() - startTime
504
+ const dataLength = apiResponse.data.length
505
+
506
+ return apiResponse.data.map((item, index) => ({
507
+ url: item.url || (item.b64_json ? `data:image/png;base64,${item.b64_json}` : ''),
508
+ prompt: `Variation ${index + 1} of source image`,
509
+ metadata: {
510
+ model,
511
+ size,
512
+ duration: Math.round(duration / dataLength),
513
+ format,
514
+ },
515
+ }))
516
+ }
517
+
518
+ /**
519
+ * Edit an image using a text prompt and optional mask
520
+ *
521
+ * @param imageUrl - URL or base64 of the source image
522
+ * @param options - Edit options including prompt and optional mask
523
+ * @returns Promise resolving to ImageResult
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * // Simple edit
528
+ * const result = await image.edit('https://example.com/image.png', {
529
+ * prompt: 'Add a rainbow in the sky',
530
+ * })
531
+ * ```
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * // Edit with mask
536
+ * const result = await image.edit('https://example.com/image.png', {
537
+ * prompt: 'A cat sitting on the couch',
538
+ * mask: 'https://example.com/mask.png', // Transparent areas will be edited
539
+ * })
540
+ * ```
541
+ */
542
+ image.edit = async function edit(imageUrl: string, options: EditOptions): Promise<ImageResult> {
543
+ const startTime = Date.now()
544
+
545
+ const {
546
+ prompt,
547
+ mask,
548
+ size = DEFAULTS.size,
549
+ model = 'dall-e-2', // DALL-E 2 supports edits
550
+ n = 1,
551
+ format = DEFAULTS.format,
552
+ } = options
553
+
554
+ const baseUrl =
555
+ getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
556
+ const endpoint = `${baseUrl}/openai/images/edits`
557
+
558
+ const formData = new FormData()
559
+
560
+ // Add source image
561
+ if (imageUrl.startsWith('http')) {
562
+ const imageResponse = await fetch(imageUrl)
563
+ const imageBlob = await imageResponse.blob()
564
+ formData.append('image', imageBlob, 'image.png')
565
+ } else if (imageUrl.startsWith('data:')) {
566
+ const blob = base64ToBlob(imageUrl)
567
+ formData.append('image', blob, 'image.png')
568
+ } else {
569
+ throw new Error('Invalid image URL format. Must be an HTTP URL or data URL.')
570
+ }
571
+
572
+ // Add mask if provided
573
+ if (mask) {
574
+ if (mask.startsWith('http')) {
575
+ const maskResponse = await fetch(mask)
576
+ const maskBlob = await maskResponse.blob()
577
+ formData.append('mask', maskBlob, 'mask.png')
578
+ } else if (mask.startsWith('data:')) {
579
+ const maskBlob = base64ToBlob(mask)
580
+ formData.append('mask', maskBlob, 'mask.png')
581
+ }
582
+ }
583
+
584
+ formData.append('prompt', prompt)
585
+ formData.append('n', String(n))
586
+ formData.append('size', size)
587
+ formData.append('response_format', format === 'b64_json' ? 'b64_json' : 'url')
588
+ if (model) {
589
+ formData.append('model', model)
590
+ }
591
+
592
+ const response = await fetch(endpoint, {
593
+ method: 'POST',
594
+ headers: getAuthHeaders(),
595
+ body: formData,
596
+ })
597
+
598
+ if (!response.ok) {
599
+ const error = await response.text()
600
+ throw new Error(`Image edit failed: ${response.status} - ${error}`)
601
+ }
602
+
603
+ const apiResponse = (await response.json()) as {
604
+ data?: Array<{
605
+ url?: string
606
+ b64_json?: string
607
+ }>
608
+ }
609
+
610
+ const imageData = apiResponse.data?.[0]
611
+
612
+ if (!imageData) {
613
+ throw new Error('No edited image returned from API')
614
+ }
615
+
616
+ const editedUrl =
617
+ imageData.url || (imageData.b64_json ? `data:image/png;base64,${imageData.b64_json}` : '')
618
+
619
+ return {
620
+ url: editedUrl,
621
+ prompt,
622
+ metadata: {
623
+ model,
624
+ size,
625
+ duration: Date.now() - startTime,
626
+ format,
627
+ },
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Upscale an image to a higher resolution
633
+ *
634
+ * @param imageUrl - URL or base64 of the source image
635
+ * @param options - Upscale options
636
+ * @returns Promise resolving to UpscaleResult
637
+ *
638
+ * @example
639
+ * ```ts
640
+ * const result = await image.upscale('https://example.com/small.png', {
641
+ * scale: 4,
642
+ * })
643
+ *
644
+ * console.log(`Upscaled from ${result.originalSize.width}x${result.originalSize.height}`)
645
+ * console.log(`to ${result.upscaledSize.width}x${result.upscaledSize.height}`)
646
+ * ```
647
+ */
648
+ image.upscale = async function upscale(
649
+ imageUrl: string,
650
+ options: UpscaleOptions = {}
651
+ ): Promise<UpscaleResult> {
652
+ const startTime = Date.now()
653
+
654
+ const { scale = 2, model = 'real-esrgan', format = DEFAULTS.format, denoise } = options
655
+
656
+ const baseUrl =
657
+ getEnv('AI_GATEWAY_URL') || getEnv('IMAGE_GATEWAY_URL') || 'https://image.workers.do'
658
+ const endpoint = `${baseUrl}/upscale`
659
+
660
+ const payload: Record<string, unknown> = {
661
+ image: imageUrl,
662
+ scale,
663
+ model,
664
+ response_format: format,
665
+ }
666
+
667
+ if (denoise !== undefined) {
668
+ payload['denoise'] = denoise
669
+ }
670
+
671
+ const response = await fetch(endpoint, {
672
+ method: 'POST',
673
+ headers: {
674
+ 'Content-Type': 'application/json',
675
+ ...getAuthHeaders(),
676
+ },
677
+ body: JSON.stringify(payload),
678
+ })
679
+
680
+ if (!response.ok) {
681
+ const error = await response.text()
682
+ throw new Error(`Image upscale failed: ${response.status} - ${error}`)
683
+ }
684
+
685
+ const apiResponse = (await response.json()) as {
686
+ url?: string
687
+ b64_json?: string
688
+ original_width?: number
689
+ original_height?: number
690
+ upscaled_width?: number
691
+ upscaled_height?: number
692
+ }
693
+
694
+ const upscaledUrl =
695
+ apiResponse.url || (apiResponse.b64_json ? `data:image/png;base64,${apiResponse.b64_json}` : '')
696
+
697
+ if (!upscaledUrl) {
698
+ throw new Error('No upscaled image returned from API')
699
+ }
700
+
701
+ // Extract dimensions from response or estimate
702
+ const originalWidth = apiResponse.original_width || 256
703
+ const originalHeight = apiResponse.original_height || 256
704
+ const upscaledWidth = apiResponse.upscaled_width || originalWidth * scale
705
+ const upscaledHeight = apiResponse.upscaled_height || originalHeight * scale
706
+
707
+ return {
708
+ url: upscaledUrl,
709
+ prompt: 'Upscaled image',
710
+ originalSize: {
711
+ width: originalWidth,
712
+ height: originalHeight,
713
+ },
714
+ upscaledSize: {
715
+ width: upscaledWidth,
716
+ height: upscaledHeight,
717
+ },
718
+ scaleFactor: scale,
719
+ metadata: {
720
+ model,
721
+ size: `${upscaledWidth}x${upscaledHeight}`,
722
+ duration: Date.now() - startTime,
723
+ format,
724
+ },
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Create a curried function for generating images with a specific style
730
+ *
731
+ * @param style - The style preset to use
732
+ * @returns A function that generates images with the specified style
733
+ *
734
+ * @example
735
+ * ```ts
736
+ * // Create a cinematic image generator
737
+ * const cinematicImage = image.style('cinematic')
738
+ *
739
+ * const result1 = await cinematicImage('A spaceship landing on Mars')
740
+ * const result2 = await cinematicImage('A detective in a rainy city')
741
+ * // Both images will have cinematic style applied
742
+ * ```
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * // Create specialized generators
747
+ * const cartoonGen = image.style('cartoon')
748
+ * const realisticGen = image.style('realistic')
749
+ * const abstractGen = image.style('abstract')
750
+ *
751
+ * // Use them throughout your app
752
+ * const cartoonAvatar = await cartoonGen('A friendly robot')
753
+ * ```
754
+ */
755
+ image.style = function style(stylePreset: ImageStyle) {
756
+ return function styledImage(
757
+ prompt: string,
758
+ options: Partial<Omit<ImageOptions, 'style'>> = {}
759
+ ): Promise<ImageResult> {
760
+ return image(prompt, { ...options, style: stylePreset })
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Batch generate multiple images from different prompts
766
+ *
767
+ * @param prompts - Array of prompts to generate
768
+ * @param options - Shared options for all generations
769
+ * @returns Promise resolving to array of ImageResults
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * const results = await image.batch([
774
+ * 'A sunset over mountains',
775
+ * 'A forest in autumn',
776
+ * 'A city at night',
777
+ * ], { style: 'photographic' })
778
+ * ```
779
+ */
780
+ image.batch = async function batch(
781
+ prompts: string[],
782
+ options: Partial<ImageOptions> = {}
783
+ ): Promise<ImageResult[]> {
784
+ return Promise.all(prompts.map((prompt) => image(prompt, options)))
785
+ }
786
+
787
+ /**
788
+ * Generate an image with a specific aspect ratio
789
+ *
790
+ * @param prompt - The text prompt
791
+ * @param aspectRatio - The desired aspect ratio ('portrait' | 'landscape' | 'square')
792
+ * @param options - Additional options
793
+ * @returns Promise resolving to ImageResult
794
+ *
795
+ * @example
796
+ * ```ts
797
+ * // Generate a portrait image
798
+ * const portrait = await image.aspectRatio('A professional headshot', 'portrait')
799
+ *
800
+ * // Generate a landscape image
801
+ * const landscape = await image.aspectRatio('Mountain range panorama', 'landscape')
802
+ * ```
803
+ */
804
+ image.aspectRatio = async function aspectRatio(
805
+ prompt: string,
806
+ ratio: 'portrait' | 'landscape' | 'square',
807
+ options: Partial<Omit<ImageOptions, 'size'>> = {}
808
+ ): Promise<ImageResult> {
809
+ const sizeMap: Record<'portrait' | 'landscape' | 'square', ImageSize> = {
810
+ portrait: '1024x1792',
811
+ landscape: '1792x1024',
812
+ square: '1024x1024',
813
+ }
814
+
815
+ return image(prompt, { ...options, size: sizeMap[ratio] })
816
+ }