@stacksjs/ai 0.70.53 → 0.70.55

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.
package/src/image.ts ADDED
@@ -0,0 +1,607 @@
1
+ /**
2
+ * AI Image Module
3
+ *
4
+ * Unified image generation and vision/analysis across multiple providers.
5
+ * Supports OpenAI DALL-E, Anthropic Claude Vision, and Ollama multimodal models.
6
+ */
7
+
8
+ import type { AIMessage, AIResult, ChatCompletionOptions } from './types'
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface ImageGenerationOptions {
15
+ provider?: 'openai' | 'ollama'
16
+ model?: string
17
+ size?: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'
18
+ quality?: 'standard' | 'hd'
19
+ n?: number
20
+ responseFormat?: 'url' | 'b64_json'
21
+ style?: 'vivid' | 'natural'
22
+ }
23
+
24
+ export interface ImageGenerationResult {
25
+ images: Array<{
26
+ url?: string
27
+ b64_json?: string
28
+ revisedPrompt?: string
29
+ }>
30
+ provider: string
31
+ model: string
32
+ }
33
+
34
+ export interface VisionOptions {
35
+ provider?: 'anthropic' | 'openai' | 'ollama'
36
+ model?: string
37
+ maxTokens?: number
38
+ temperature?: number
39
+ detail?: 'auto' | 'low' | 'high'
40
+ }
41
+
42
+ export interface VisionResult extends AIResult {
43
+ provider: string
44
+ }
45
+
46
+ export type ImageInput =
47
+ | { type: 'url'; url: string }
48
+ | { type: 'base64'; data: string; mediaType: string }
49
+ | { type: 'file'; path: string }
50
+
51
+ // ============================================================================
52
+ // Image Generation
53
+ // ============================================================================
54
+
55
+ /**
56
+ * Generate images from a text prompt.
57
+ * Currently supports OpenAI DALL-E models.
58
+ */
59
+ export async function generateImage(
60
+ prompt: string,
61
+ options: ImageGenerationOptions = {},
62
+ ): Promise<ImageGenerationResult> {
63
+ const {
64
+ provider = 'openai',
65
+ model = 'dall-e-3',
66
+ size = '1024x1024',
67
+ quality = 'standard',
68
+ n = 1,
69
+ responseFormat = 'url',
70
+ style = 'vivid',
71
+ } = options
72
+
73
+ if (provider === 'openai') {
74
+ return generateImageOpenAI(prompt, { model, size, quality, n, responseFormat, style })
75
+ }
76
+
77
+ throw new Error(`Image generation not supported for provider: ${provider}. Use 'openai'.`)
78
+ }
79
+
80
+ async function generateImageOpenAI(
81
+ prompt: string,
82
+ options: {
83
+ model: string
84
+ size: string
85
+ quality: string
86
+ n: number
87
+ responseFormat: string
88
+ style: string
89
+ },
90
+ ): Promise<ImageGenerationResult> {
91
+ const apiKey = process.env.OPENAI_API_KEY
92
+ if (!apiKey) {
93
+ throw new Error('OPENAI_API_KEY environment variable is required for image generation.')
94
+ }
95
+
96
+ const response = await fetch('https://api.openai.com/v1/images/generations', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'Authorization': `Bearer ${apiKey}`,
101
+ },
102
+ body: JSON.stringify({
103
+ model: options.model,
104
+ prompt,
105
+ size: options.size,
106
+ quality: options.quality,
107
+ n: options.n,
108
+ response_format: options.responseFormat,
109
+ style: options.style,
110
+ }),
111
+ })
112
+
113
+ if (!response.ok) {
114
+ const error = await response.text()
115
+ throw new Error(`OpenAI Image API error: ${error}`)
116
+ }
117
+
118
+ const data = (await response.json()) as {
119
+ data: Array<{ url?: string; b64_json?: string; revised_prompt?: string }>
120
+ }
121
+
122
+ return {
123
+ images: data.data.map(img => ({
124
+ url: img.url,
125
+ b64_json: img.b64_json,
126
+ revisedPrompt: img.revised_prompt,
127
+ })),
128
+ provider: 'openai',
129
+ model: options.model,
130
+ }
131
+ }
132
+
133
+ // ============================================================================
134
+ // Image Editing
135
+ // ============================================================================
136
+
137
+ export interface ImageEditOptions {
138
+ mask?: Blob | File
139
+ model?: 'dall-e-2'
140
+ n?: number
141
+ size?: '256x256' | '512x512' | '1024x1024'
142
+ responseFormat?: 'url' | 'b64_json'
143
+ }
144
+
145
+ /**
146
+ * Edit an existing image with a text prompt (OpenAI DALL-E 2).
147
+ */
148
+ export async function editImage(
149
+ image: Blob | File,
150
+ prompt: string,
151
+ options: ImageEditOptions = {},
152
+ ): Promise<ImageGenerationResult> {
153
+ const apiKey = process.env.OPENAI_API_KEY
154
+ if (!apiKey) {
155
+ throw new Error('OPENAI_API_KEY environment variable is required for image editing.')
156
+ }
157
+
158
+ const formData = new FormData()
159
+ formData.append('image', image)
160
+ formData.append('prompt', prompt)
161
+ formData.append('model', options.model || 'dall-e-2')
162
+ if (options.mask) formData.append('mask', options.mask)
163
+ if (options.n) formData.append('n', String(options.n))
164
+ if (options.size) formData.append('size', options.size)
165
+ if (options.responseFormat) formData.append('response_format', options.responseFormat)
166
+
167
+ const response = await fetch('https://api.openai.com/v1/images/edits', {
168
+ method: 'POST',
169
+ headers: {
170
+ 'Authorization': `Bearer ${apiKey}`,
171
+ },
172
+ body: formData,
173
+ })
174
+
175
+ if (!response.ok) {
176
+ const error = await response.text()
177
+ throw new Error(`OpenAI Image Edit API error: ${error}`)
178
+ }
179
+
180
+ const data = (await response.json()) as {
181
+ data: Array<{ url?: string; b64_json?: string }>
182
+ }
183
+
184
+ return {
185
+ images: data.data.map(img => ({
186
+ url: img.url,
187
+ b64_json: img.b64_json,
188
+ })),
189
+ provider: 'openai',
190
+ model: options.model || 'dall-e-2',
191
+ }
192
+ }
193
+
194
+ // ============================================================================
195
+ // Image Variations
196
+ // ============================================================================
197
+
198
+ /**
199
+ * Create variations of an existing image (OpenAI DALL-E 2).
200
+ */
201
+ export async function createImageVariation(
202
+ image: Blob | File,
203
+ options: {
204
+ model?: 'dall-e-2'
205
+ n?: number
206
+ size?: '256x256' | '512x512' | '1024x1024'
207
+ responseFormat?: 'url' | 'b64_json'
208
+ } = {},
209
+ ): Promise<ImageGenerationResult> {
210
+ const apiKey = process.env.OPENAI_API_KEY
211
+ if (!apiKey) {
212
+ throw new Error('OPENAI_API_KEY environment variable is required for image variations.')
213
+ }
214
+
215
+ const formData = new FormData()
216
+ formData.append('image', image)
217
+ formData.append('model', options.model || 'dall-e-2')
218
+ if (options.n) formData.append('n', String(options.n))
219
+ if (options.size) formData.append('size', options.size)
220
+ if (options.responseFormat) formData.append('response_format', options.responseFormat)
221
+
222
+ const response = await fetch('https://api.openai.com/v1/images/variations', {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Authorization': `Bearer ${apiKey}`,
226
+ },
227
+ body: formData,
228
+ })
229
+
230
+ if (!response.ok) {
231
+ const error = await response.text()
232
+ throw new Error(`OpenAI Image Variations API error: ${error}`)
233
+ }
234
+
235
+ const data = (await response.json()) as {
236
+ data: Array<{ url?: string; b64_json?: string }>
237
+ }
238
+
239
+ return {
240
+ images: data.data.map(img => ({
241
+ url: img.url,
242
+ b64_json: img.b64_json,
243
+ })),
244
+ provider: 'openai',
245
+ model: options.model || 'dall-e-2',
246
+ }
247
+ }
248
+
249
+ // ============================================================================
250
+ // Vision / Image Analysis
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Analyze an image using AI vision capabilities.
255
+ * Supports Anthropic Claude, OpenAI GPT-4V, and Ollama multimodal models.
256
+ */
257
+ export async function analyzeImage(
258
+ imageInput: ImageInput,
259
+ prompt: string,
260
+ options: VisionOptions = {},
261
+ ): Promise<VisionResult> {
262
+ const { provider = 'anthropic' } = options
263
+
264
+ switch (provider) {
265
+ case 'anthropic':
266
+ return analyzeImageAnthropic(imageInput, prompt, options)
267
+ case 'openai':
268
+ return analyzeImageOpenAI(imageInput, prompt, options)
269
+ case 'ollama':
270
+ return analyzeImageOllama(imageInput, prompt, options)
271
+ default:
272
+ throw new Error(`Vision not supported for provider: ${provider}`)
273
+ }
274
+ }
275
+
276
+ async function resolveImageToBase64(input: ImageInput): Promise<{ data: string; mediaType: string }> {
277
+ if (input.type === 'base64') {
278
+ return { data: input.data, mediaType: input.mediaType }
279
+ }
280
+
281
+ if (input.type === 'url') {
282
+ const response = await fetch(input.url)
283
+ if (!response.ok) throw new Error(`Failed to fetch image from URL: ${input.url}`)
284
+ const buffer = await response.arrayBuffer()
285
+ const base64 = Buffer.from(buffer).toString('base64')
286
+ const contentType = response.headers.get('content-type') || 'image/png'
287
+ return { data: base64, mediaType: contentType }
288
+ }
289
+
290
+ if (input.type === 'file') {
291
+ const { readFile } = await import('node:fs/promises')
292
+ const buffer = await readFile(input.path)
293
+ const base64 = buffer.toString('base64')
294
+ const ext = input.path.split('.').pop()?.toLowerCase() || 'png'
295
+ const mediaTypes: Record<string, string> = {
296
+ png: 'image/png',
297
+ jpg: 'image/jpeg',
298
+ jpeg: 'image/jpeg',
299
+ gif: 'image/gif',
300
+ webp: 'image/webp',
301
+ }
302
+ return { data: base64, mediaType: mediaTypes[ext] || 'image/png' }
303
+ }
304
+
305
+ throw new Error('Invalid image input type')
306
+ }
307
+
308
+ async function analyzeImageAnthropic(
309
+ imageInput: ImageInput,
310
+ prompt: string,
311
+ options: VisionOptions,
312
+ ): Promise<VisionResult> {
313
+ const apiKey = process.env.ANTHROPIC_API_KEY
314
+ if (!apiKey) {
315
+ throw new Error('ANTHROPIC_API_KEY environment variable is required for Claude vision.')
316
+ }
317
+
318
+ const { data, mediaType } = await resolveImageToBase64(imageInput)
319
+ const model = options.model || 'claude-sonnet-4-20250514'
320
+ const maxTokens = options.maxTokens || 4096
321
+
322
+ const messages: AIMessage[] = [{
323
+ role: 'user',
324
+ content: [
325
+ {
326
+ type: 'image',
327
+ source: { type: 'base64', media_type: mediaType, data },
328
+ },
329
+ { type: 'text', text: prompt },
330
+ ],
331
+ }]
332
+
333
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
334
+ method: 'POST',
335
+ headers: {
336
+ 'Content-Type': 'application/json',
337
+ 'x-api-key': apiKey,
338
+ 'anthropic-version': '2023-06-01',
339
+ },
340
+ body: JSON.stringify({
341
+ model,
342
+ max_tokens: maxTokens,
343
+ temperature: options.temperature,
344
+ messages,
345
+ }),
346
+ })
347
+
348
+ if (!response.ok) {
349
+ const error = await response.text()
350
+ throw new Error(`Claude Vision API error: ${error}`)
351
+ }
352
+
353
+ const responseData = (await response.json()) as any
354
+
355
+ return {
356
+ content: responseData.content[0].text,
357
+ model: responseData.model,
358
+ provider: 'anthropic',
359
+ usage: {
360
+ promptTokens: responseData.usage?.input_tokens || 0,
361
+ completionTokens: responseData.usage?.output_tokens || 0,
362
+ totalTokens: (responseData.usage?.input_tokens || 0) + (responseData.usage?.output_tokens || 0),
363
+ },
364
+ finishReason: responseData.stop_reason,
365
+ }
366
+ }
367
+
368
+ async function analyzeImageOpenAI(
369
+ imageInput: ImageInput,
370
+ prompt: string,
371
+ options: VisionOptions,
372
+ ): Promise<VisionResult> {
373
+ const apiKey = process.env.OPENAI_API_KEY
374
+ if (!apiKey) {
375
+ throw new Error('OPENAI_API_KEY environment variable is required for GPT-4 vision.')
376
+ }
377
+
378
+ const model = options.model || 'gpt-4o'
379
+ const maxTokens = options.maxTokens || 4096
380
+ const detail = options.detail || 'auto'
381
+
382
+ let imageContent: any
383
+ if (imageInput.type === 'url') {
384
+ imageContent = { type: 'image_url', image_url: { url: imageInput.url, detail } }
385
+ }
386
+ else {
387
+ const { data, mediaType } = await resolveImageToBase64(imageInput)
388
+ imageContent = {
389
+ type: 'image_url',
390
+ image_url: { url: `data:${mediaType};base64,${data}`, detail },
391
+ }
392
+ }
393
+
394
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
395
+ method: 'POST',
396
+ headers: {
397
+ 'Content-Type': 'application/json',
398
+ 'Authorization': `Bearer ${apiKey}`,
399
+ },
400
+ body: JSON.stringify({
401
+ model,
402
+ max_tokens: maxTokens,
403
+ temperature: options.temperature,
404
+ messages: [{
405
+ role: 'user',
406
+ content: [
407
+ imageContent,
408
+ { type: 'text', text: prompt },
409
+ ],
410
+ }],
411
+ }),
412
+ })
413
+
414
+ if (!response.ok) {
415
+ const error = await response.text()
416
+ throw new Error(`OpenAI Vision API error: ${error}`)
417
+ }
418
+
419
+ const responseData = (await response.json()) as any
420
+
421
+ return {
422
+ content: responseData.choices[0].message.content,
423
+ model: responseData.model,
424
+ provider: 'openai',
425
+ usage: {
426
+ promptTokens: responseData.usage?.prompt_tokens || 0,
427
+ completionTokens: responseData.usage?.completion_tokens || 0,
428
+ totalTokens: responseData.usage?.total_tokens || 0,
429
+ },
430
+ finishReason: responseData.choices[0].finish_reason,
431
+ }
432
+ }
433
+
434
+ async function analyzeImageOllama(
435
+ imageInput: ImageInput,
436
+ prompt: string,
437
+ options: VisionOptions,
438
+ ): Promise<VisionResult> {
439
+ const host = process.env.OLLAMA_HOST || 'http://localhost:11434'
440
+ const model = options.model || 'llava'
441
+
442
+ const { data } = await resolveImageToBase64(imageInput)
443
+
444
+ const response = await fetch(`${host}/api/generate`, {
445
+ method: 'POST',
446
+ headers: { 'Content-Type': 'application/json' },
447
+ body: JSON.stringify({
448
+ model,
449
+ prompt,
450
+ images: [data],
451
+ stream: false,
452
+ }),
453
+ })
454
+
455
+ if (!response.ok) {
456
+ const error = await response.text()
457
+ throw new Error(`Ollama Vision API error: ${error}`)
458
+ }
459
+
460
+ const responseData = (await response.json()) as any
461
+
462
+ return {
463
+ content: responseData.response,
464
+ model: responseData.model,
465
+ provider: 'ollama',
466
+ usage: {
467
+ promptTokens: responseData.prompt_eval_count || 0,
468
+ completionTokens: responseData.eval_count || 0,
469
+ totalTokens: (responseData.prompt_eval_count || 0) + (responseData.eval_count || 0),
470
+ },
471
+ }
472
+ }
473
+
474
+ // ============================================================================
475
+ // Multi-image Analysis
476
+ // ============================================================================
477
+
478
+ /**
479
+ * Analyze multiple images together with a prompt.
480
+ * Useful for comparison, batch analysis, etc.
481
+ */
482
+ export async function analyzeImages(
483
+ images: ImageInput[],
484
+ prompt: string,
485
+ options: VisionOptions = {},
486
+ ): Promise<VisionResult> {
487
+ const { provider = 'anthropic' } = options
488
+
489
+ if (provider === 'anthropic') {
490
+ const apiKey = process.env.ANTHROPIC_API_KEY
491
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY required for multi-image analysis.')
492
+
493
+ const model = options.model || 'claude-sonnet-4-20250514'
494
+ const contentBlocks: any[] = []
495
+
496
+ for (const img of images) {
497
+ const { data, mediaType } = await resolveImageToBase64(img)
498
+ contentBlocks.push({
499
+ type: 'image',
500
+ source: { type: 'base64', media_type: mediaType, data },
501
+ })
502
+ }
503
+ contentBlocks.push({ type: 'text', text: prompt })
504
+
505
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
506
+ method: 'POST',
507
+ headers: {
508
+ 'Content-Type': 'application/json',
509
+ 'x-api-key': apiKey,
510
+ 'anthropic-version': '2023-06-01',
511
+ },
512
+ body: JSON.stringify({
513
+ model,
514
+ max_tokens: options.maxTokens || 4096,
515
+ temperature: options.temperature,
516
+ messages: [{ role: 'user', content: contentBlocks }],
517
+ }),
518
+ })
519
+
520
+ if (!response.ok) {
521
+ const error = await response.text()
522
+ throw new Error(`Claude Vision API error: ${error}`)
523
+ }
524
+
525
+ const responseData = (await response.json()) as any
526
+ return {
527
+ content: responseData.content[0].text,
528
+ model: responseData.model,
529
+ provider: 'anthropic',
530
+ usage: {
531
+ promptTokens: responseData.usage?.input_tokens || 0,
532
+ completionTokens: responseData.usage?.output_tokens || 0,
533
+ totalTokens: (responseData.usage?.input_tokens || 0) + (responseData.usage?.output_tokens || 0),
534
+ },
535
+ finishReason: responseData.stop_reason,
536
+ }
537
+ }
538
+
539
+ if (provider === 'openai') {
540
+ const apiKey = process.env.OPENAI_API_KEY
541
+ if (!apiKey) throw new Error('OPENAI_API_KEY required for multi-image analysis.')
542
+
543
+ const model = options.model || 'gpt-4o'
544
+ const detail = options.detail || 'auto'
545
+ const contentBlocks: any[] = []
546
+
547
+ for (const img of images) {
548
+ if (img.type === 'url') {
549
+ contentBlocks.push({ type: 'image_url', image_url: { url: img.url, detail } })
550
+ }
551
+ else {
552
+ const { data, mediaType } = await resolveImageToBase64(img)
553
+ contentBlocks.push({
554
+ type: 'image_url',
555
+ image_url: { url: `data:${mediaType};base64,${data}`, detail },
556
+ })
557
+ }
558
+ }
559
+ contentBlocks.push({ type: 'text', text: prompt })
560
+
561
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
562
+ method: 'POST',
563
+ headers: {
564
+ 'Content-Type': 'application/json',
565
+ 'Authorization': `Bearer ${apiKey}`,
566
+ },
567
+ body: JSON.stringify({
568
+ model,
569
+ max_tokens: options.maxTokens || 4096,
570
+ temperature: options.temperature,
571
+ messages: [{ role: 'user', content: contentBlocks }],
572
+ }),
573
+ })
574
+
575
+ if (!response.ok) {
576
+ const error = await response.text()
577
+ throw new Error(`OpenAI Vision API error: ${error}`)
578
+ }
579
+
580
+ const responseData = (await response.json()) as any
581
+ return {
582
+ content: responseData.choices[0].message.content,
583
+ model: responseData.model,
584
+ provider: 'openai',
585
+ usage: {
586
+ promptTokens: responseData.usage?.prompt_tokens || 0,
587
+ completionTokens: responseData.usage?.completion_tokens || 0,
588
+ totalTokens: responseData.usage?.total_tokens || 0,
589
+ },
590
+ finishReason: responseData.choices[0].finish_reason,
591
+ }
592
+ }
593
+
594
+ throw new Error(`Multi-image analysis not supported for provider: ${provider}`)
595
+ }
596
+
597
+ // ============================================================================
598
+ // Exports
599
+ // ============================================================================
600
+
601
+ export const image = {
602
+ generate: generateImage,
603
+ edit: editImage,
604
+ variation: createImageVariation,
605
+ analyze: analyzeImage,
606
+ analyzeMultiple: analyzeImages,
607
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Types
2
+ export * from './types'
3
+
4
+ // Drivers
5
+ export * from './drivers'
6
+
7
+ // Agents
8
+ export * from './agents'
9
+
10
+ // Buddy - Voice AI Code Assistant
11
+ export * from './buddy'
12
+
13
+ // Text utilities
14
+ export * from './text'
15
+
16
+ // Image generation & vision
17
+ export * from './image'
18
+
19
+ // Semantic search, embeddings & RAG
20
+ export * from './search'
21
+
22
+ // Personalization, sentiment & classification
23
+ export * from './personalization'
24
+
25
+ // Model Context Protocol (MCP) client
26
+ export * from './mcp'
27
+
28
+ // AWS Bedrock utilities
29
+ export * from './utils/client-bedrock'
30
+ export * from './utils/client-bedrock-runtime'
31
+
32
+ // Cross-driver vision helpers (stacksjs/stacks#1878 A-3).
33
+ // `buildMessageWithImages(command, images)` constructs portable
34
+ // content arrays; `normalizeMessagesForProvider(messages, 'openai' | 'anthropic')`
35
+ // translates between the OpenAI image_url and Anthropic image
36
+ // source formats so apps can switch providers without rewriting.
37
+ export { buildMessageWithImages, normalizeMessagesForProvider } from './utils/vision'
38
+
39
+ // HTTP retry helper for 429/5xx (stacksjs/stacks#1878 A-5).
40
+ export { fetchWithRetry } from './utils/retry'
41
+ export type { RetryConfig } from './utils/retry'
42
+
43
+ // Usage tracking (stacksjs/stacks#1878 A-6). Apps install reporters
44
+ // via `onUsage(fn)`; drivers fire `recordUsage(...)` per completion.
45
+ // Default with no reporters is a no-op.
46
+ export { clearUsageReporters, listUsageReporters, onUsage, recordUsage } from './utils/usage'
47
+ export type { UsageRecord, UsageReporter } from './utils/usage'
48
+
49
+ // Token estimation + prompt-injection heuristics (stacksjs/stacks#1878 A-7).
50
+ export { estimateMessageTokens, estimateTokens, sanitizePrompt } from './utils/tokens'
51
+ export type { SanitizeResult } from './utils/tokens'
52
+
53
+ export * from './utils/model-access'