@stacksjs/ai 0.70.87 → 0.70.90

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/dist/search.js ADDED
@@ -0,0 +1,316 @@
1
+ export async function createEmbedding(input, options = {}) {
2
+ const { provider = "openai" } = options;
3
+ if (provider === "openai")
4
+ return createEmbeddingOpenAI(input, options.model || "text-embedding-3-small");
5
+ if (provider === "ollama")
6
+ return createEmbeddingOllama(input, options.model || "nomic-embed-text");
7
+ throw Error(`Embedding provider not supported: ${provider}`);
8
+ }
9
+ async function createEmbeddingOpenAI(input, model) {
10
+ const apiKey = process.env.OPENAI_API_KEY;
11
+ if (!apiKey)
12
+ throw Error("OPENAI_API_KEY environment variable is required for embeddings.");
13
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
14
+ method: "POST",
15
+ headers: {
16
+ "Content-Type": "application/json",
17
+ Authorization: `Bearer ${apiKey}`
18
+ },
19
+ body: JSON.stringify({ model, input })
20
+ });
21
+ if (!response.ok) {
22
+ const error = await response.text();
23
+ throw Error(`OpenAI Embeddings API error: ${error}`);
24
+ }
25
+ const data = await response.json();
26
+ if (Array.isArray(input))
27
+ return data.data.map((d) => d.embedding);
28
+ const first = data.data[0];
29
+ if (!first)
30
+ throw Error("OpenAI Embeddings API returned no embedding");
31
+ return first.embedding;
32
+ }
33
+ async function createEmbeddingOllama(input, model) {
34
+ const host = process.env.OLLAMA_HOST || "http://localhost:11434", inputs = Array.isArray(input) ? input : [input], embeddings = [];
35
+ for (const text of inputs) {
36
+ const response = await fetch(`${host}/api/embeddings`, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ body: JSON.stringify({ model, prompt: text })
40
+ });
41
+ if (!response.ok) {
42
+ const error = await response.text();
43
+ throw Error(`Ollama Embeddings API error: ${error}`);
44
+ }
45
+ const data = await response.json();
46
+ embeddings.push(data.embedding);
47
+ }
48
+ if (Array.isArray(input))
49
+ return embeddings;
50
+ const first = embeddings[0];
51
+ if (!first)
52
+ throw Error("Ollama Embeddings API returned no embedding");
53
+ return first;
54
+ }
55
+ export function cosineSimilarity(a, b) {
56
+ if (a.length !== b.length)
57
+ throw Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
58
+ let dotProduct = 0, normA = 0, normB = 0;
59
+ for (let i = 0;i < a.length; i++) {
60
+ const av = a[i], bv = b[i];
61
+ dotProduct += av * bv;
62
+ normA += av * av;
63
+ normB += bv * bv;
64
+ }
65
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
66
+ if (denominator === 0)
67
+ return 0;
68
+ return dotProduct / denominator;
69
+ }
70
+ export function dotProduct(a, b) {
71
+ if (a.length !== b.length)
72
+ throw Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
73
+ let result = 0;
74
+ for (let i = 0;i < a.length; i++)
75
+ result += a[i] * b[i];
76
+ return result;
77
+ }
78
+ export function euclideanDistance(a, b) {
79
+ if (a.length !== b.length)
80
+ throw Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
81
+ let sum = 0;
82
+ for (let i = 0;i < a.length; i++) {
83
+ const diff = a[i] - b[i];
84
+ sum += diff * diff;
85
+ }
86
+ return Math.sqrt(sum);
87
+ }
88
+
89
+ export class VectorIndex {
90
+ documents = [];
91
+ embeddingOptions;
92
+ constructor(options = {}) {
93
+ this.embeddingOptions = options;
94
+ }
95
+ async add(documents) {
96
+ if (documents.length === 0)
97
+ return;
98
+ const contents = documents.map((d) => d.content), embeddings = await createEmbedding(contents, this.embeddingOptions);
99
+ for (let i = 0;i < documents.length; i++) {
100
+ const document = documents[i], embedding = embeddings[i];
101
+ if (!document || !embedding)
102
+ continue;
103
+ this.documents.push({
104
+ ...document,
105
+ embedding
106
+ });
107
+ }
108
+ }
109
+ addWithEmbedding(document, embedding) {
110
+ this.documents.push({ ...document, embedding });
111
+ }
112
+ async search(query, topK = 5) {
113
+ if (this.documents.length === 0)
114
+ return [];
115
+ const queryEmbedding = await createEmbedding(query, this.embeddingOptions);
116
+ return this.searchByVector(queryEmbedding, topK);
117
+ }
118
+ searchByVector(queryEmbedding, topK = 5) {
119
+ if (this.documents.length === 0)
120
+ return [];
121
+ const scored = this.documents.map((doc) => ({
122
+ document: { id: doc.id, content: doc.content, metadata: doc.metadata },
123
+ score: cosineSimilarity(queryEmbedding, doc.embedding)
124
+ }));
125
+ scored.sort((a, b) => b.score - a.score);
126
+ return scored.slice(0, topK).map((result, index) => ({
127
+ ...result,
128
+ rank: index + 1
129
+ }));
130
+ }
131
+ remove(id) {
132
+ const initialLength = this.documents.length;
133
+ this.documents = this.documents.filter((d) => d.id !== id);
134
+ return this.documents.length < initialLength;
135
+ }
136
+ clear() {
137
+ this.documents = [];
138
+ }
139
+ get size() {
140
+ return this.documents.length;
141
+ }
142
+ get ids() {
143
+ return this.documents.map((d) => d.id);
144
+ }
145
+ }
146
+ export async function rag(query, index, options = {}) {
147
+ const {
148
+ provider = "anthropic",
149
+ topK = 5,
150
+ maxTokens = 4096,
151
+ temperature,
152
+ systemPrompt
153
+ } = options, searchResults = await index.search(query, topK), contextText = searchResults.map((r, i) => `[Source ${i + 1}] (score: ${r.score.toFixed(3)})
154
+ ${r.document.content}`).join(`
155
+
156
+ ---
157
+
158
+ `), ragSystemPrompt = systemPrompt || "You are a helpful assistant. Answer the user's question based on the provided context. If the context doesn't contain relevant information, say so. Always cite your sources by referencing [Source N].", fullPrompt = `Context:
159
+ ${contextText}
160
+
161
+ Question: ${query}`;
162
+ if (provider === "anthropic")
163
+ return ragWithAnthropic(fullPrompt, ragSystemPrompt, searchResults, { maxTokens, temperature, model: options.model });
164
+ if (provider === "openai")
165
+ return ragWithOpenAI(fullPrompt, ragSystemPrompt, searchResults, { maxTokens, temperature, model: options.model });
166
+ if (provider === "ollama")
167
+ return ragWithOllama(fullPrompt, ragSystemPrompt, searchResults, { maxTokens, temperature, model: options.model });
168
+ throw Error(`RAG provider not supported: ${provider}`);
169
+ }
170
+ async function ragWithAnthropic(prompt, systemPrompt, sources, options) {
171
+ const apiKey = process.env.ANTHROPIC_API_KEY;
172
+ if (!apiKey)
173
+ throw Error("ANTHROPIC_API_KEY required for RAG with Anthropic.");
174
+ const model = options.model || "claude-sonnet-4-20250514", response = await fetch("https://api.anthropic.com/v1/messages", {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ "x-api-key": apiKey,
179
+ "anthropic-version": "2023-06-01"
180
+ },
181
+ body: JSON.stringify({
182
+ model,
183
+ max_tokens: options.maxTokens || 4096,
184
+ temperature: options.temperature,
185
+ system: systemPrompt,
186
+ messages: [{ role: "user", content: prompt }]
187
+ })
188
+ });
189
+ if (!response.ok) {
190
+ const error = await response.text();
191
+ throw Error(`Claude API error: ${error}`);
192
+ }
193
+ const data = await response.json();
194
+ return {
195
+ content: data.content[0].text,
196
+ model: data.model,
197
+ usage: {
198
+ promptTokens: data.usage?.input_tokens || 0,
199
+ completionTokens: data.usage?.output_tokens || 0,
200
+ totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
201
+ },
202
+ finishReason: data.stop_reason,
203
+ sources
204
+ };
205
+ }
206
+ async function ragWithOpenAI(prompt, systemPrompt, sources, options) {
207
+ const apiKey = process.env.OPENAI_API_KEY;
208
+ if (!apiKey)
209
+ throw Error("OPENAI_API_KEY required for RAG with OpenAI.");
210
+ const model = options.model || "gpt-4o", response = await fetch("https://api.openai.com/v1/chat/completions", {
211
+ method: "POST",
212
+ headers: {
213
+ "Content-Type": "application/json",
214
+ Authorization: `Bearer ${apiKey}`
215
+ },
216
+ body: JSON.stringify({
217
+ model,
218
+ max_tokens: options.maxTokens || 4096,
219
+ temperature: options.temperature,
220
+ messages: [
221
+ { role: "system", content: systemPrompt },
222
+ { role: "user", content: prompt }
223
+ ]
224
+ })
225
+ });
226
+ if (!response.ok) {
227
+ const error = await response.text();
228
+ throw Error(`OpenAI API error: ${error}`);
229
+ }
230
+ const data = await response.json();
231
+ return {
232
+ content: data.choices[0].message.content,
233
+ model: data.model,
234
+ usage: {
235
+ promptTokens: data.usage?.prompt_tokens || 0,
236
+ completionTokens: data.usage?.completion_tokens || 0,
237
+ totalTokens: data.usage?.total_tokens || 0
238
+ },
239
+ finishReason: data.choices[0].finish_reason,
240
+ sources
241
+ };
242
+ }
243
+ async function ragWithOllama(prompt, systemPrompt, sources, options) {
244
+ const host = process.env.OLLAMA_HOST || "http://localhost:11434", model = options.model || "llama3.2", response = await fetch(`${host}/api/chat`, {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({
248
+ model,
249
+ messages: [
250
+ { role: "system", content: systemPrompt },
251
+ { role: "user", content: prompt }
252
+ ],
253
+ stream: !1,
254
+ options: {
255
+ temperature: options.temperature
256
+ }
257
+ })
258
+ });
259
+ if (!response.ok) {
260
+ const error = await response.text();
261
+ throw Error(`Ollama API error: ${error}`);
262
+ }
263
+ const data = await response.json();
264
+ return {
265
+ content: data.message.content,
266
+ model: data.model,
267
+ usage: {
268
+ promptTokens: data.prompt_eval_count || 0,
269
+ completionTokens: data.eval_count || 0,
270
+ totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
271
+ },
272
+ finishReason: data.done_reason || "stop",
273
+ sources
274
+ };
275
+ }
276
+ export function chunkText(text, options = {}) {
277
+ const {
278
+ chunkSize = 1000,
279
+ chunkOverlap = 200,
280
+ separator = `
281
+ `
282
+ } = options, segments = text.split(separator), chunks = [];
283
+ let currentChunk = "";
284
+ for (const segment of segments)
285
+ if (currentChunk.length + segment.length + 1 > chunkSize && currentChunk.length > 0) {
286
+ chunks.push(currentChunk.trim());
287
+ if (chunkOverlap > 0) {
288
+ const overlapStart = Math.max(0, currentChunk.length - chunkOverlap);
289
+ currentChunk = currentChunk.slice(overlapStart) + separator + segment;
290
+ } else
291
+ currentChunk = segment;
292
+ } else
293
+ currentChunk += (currentChunk ? separator : "") + segment;
294
+ if (currentChunk.trim())
295
+ chunks.push(currentChunk.trim());
296
+ return chunks;
297
+ }
298
+ export async function indexText(text, options = {}) {
299
+ const chunks = chunkText(text, options), index = new VectorIndex(options), documents = chunks.map((chunk, i) => ({
300
+ id: `${options.idPrefix || "chunk"}-${i}`,
301
+ content: chunk,
302
+ metadata: { chunkIndex: i, totalChunks: chunks.length }
303
+ }));
304
+ await index.add(documents);
305
+ return index;
306
+ }
307
+ export const search = {
308
+ createEmbedding,
309
+ cosineSimilarity,
310
+ dotProduct,
311
+ euclideanDistance,
312
+ VectorIndex,
313
+ rag,
314
+ chunkText,
315
+ indexText
316
+ };
package/dist/text.js ADDED
@@ -0,0 +1,51 @@
1
+ import { invokeModel } from "./utils/client-bedrock-runtime";
2
+ const DEFAULT_MODEL = "amazon.titan-text-express-v1";
3
+ function resolveModel(override) {
4
+ if (override)
5
+ return override;
6
+ return globalThis.config?.ai?.bedrock?.model || process.env.BEDROCK_MODEL_ID || DEFAULT_MODEL;
7
+ }
8
+ export async function summarize(text, options = {}) {
9
+ const { maxTokenCount = 512, temperature = 0, topP = 0.9, modelId } = options;
10
+ try {
11
+ const response = await invokeModel({
12
+ modelId: resolveModel(modelId),
13
+ contentType: "application/json",
14
+ accept: "*/*",
15
+ body: JSON.stringify({
16
+ inputText: `Summarize the following text: ${text}`,
17
+ textGenerationConfig: {
18
+ maxTokenCount,
19
+ stopSequences: [],
20
+ temperature,
21
+ topP
22
+ }
23
+ })
24
+ });
25
+ return JSON.parse(new TextDecoder().decode(response.body)).results[0].outputText;
26
+ } catch (error) {
27
+ throw Error(`Error summarizing text: ${error.message}`);
28
+ }
29
+ }
30
+ export async function ask(question, options = {}) {
31
+ const { maxTokenCount = 512, temperature = 0, topP = 0.9, modelId } = options;
32
+ try {
33
+ const response = await invokeModel({
34
+ modelId: resolveModel(modelId),
35
+ contentType: "application/json",
36
+ accept: "*/*",
37
+ body: JSON.stringify({
38
+ inputText: question,
39
+ textGenerationConfig: {
40
+ maxTokenCount,
41
+ stopSequences: [],
42
+ temperature,
43
+ topP
44
+ }
45
+ })
46
+ });
47
+ return JSON.parse(new TextDecoder().decode(response.body)).results[0].outputText;
48
+ } catch (error) {
49
+ throw Error(`Error asking question: ${error.message}`);
50
+ }
51
+ }
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,17 @@
1
+ import process from "node:process";
2
+ let _client = null;
3
+ async function getClient() {
4
+ if (_client)
5
+ return _client;
6
+ const mod = await import("@stacksjs/ts-cloud/aws");
7
+ if (!mod?.BedrockRuntimeClient)
8
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockRuntimeClient \u2014 rebuild ts-cloud or remove the AI dependency.");
9
+ _client = new mod.BedrockRuntimeClient(process.env.REGION || "us-east-1");
10
+ return _client;
11
+ }
12
+ export async function invokeModel(params) {
13
+ return (await getClient()).invokeModel(params);
14
+ }
15
+ export async function invokeModelWithResponseStream(params) {
16
+ return (await getClient()).invokeModelWithResponseStream(params);
17
+ }
@@ -0,0 +1,20 @@
1
+ import process from "node:process";
2
+ let _client = null;
3
+ async function getClient() {
4
+ if (_client)
5
+ return _client;
6
+ const mod = await import("@stacksjs/ts-cloud/aws");
7
+ if (!mod?.BedrockClient)
8
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockClient \u2014 rebuild ts-cloud or remove the AI dependency.");
9
+ _client = new mod.BedrockClient(process.env.REGION || "us-east-1");
10
+ return _client;
11
+ }
12
+ export async function createModelCustomizationJob(param) {
13
+ return (await getClient()).createModelCustomizationJob(param);
14
+ }
15
+ export async function getModelCustomizationJob(params) {
16
+ return (await getClient()).getModelCustomizationJob(params);
17
+ }
18
+ export async function listFoundationModels(params) {
19
+ return (await getClient()).listFoundationModels(params);
20
+ }
@@ -0,0 +1,21 @@
1
+ import { log } from "@stacksjs/cli";
2
+ import { ai } from "@stacksjs/config";
3
+ async function getBedrockClient() {
4
+ const mod = await import("@stacksjs/ts-cloud/aws");
5
+ if (!mod?.BedrockClient)
6
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockClient \u2014 rebuild ts-cloud or remove the AI dependency.");
7
+ return new mod.BedrockClient("us-east-1");
8
+ }
9
+ export async function requestModelAccess() {
10
+ const client = await getBedrockClient(), models = ai.models;
11
+ if (!models)
12
+ throw Error("No AI models found. Please set ./config/ai.ts values.");
13
+ for (const model of models)
14
+ try {
15
+ log.info(`Requesting access to model ${model}`);
16
+ const data = await client.requestModelAccess({ modelId: model });
17
+ log.info(`Response for model ${model}:`, data);
18
+ } catch (error) {
19
+ log.error(`Error requesting access to model ${model}:`, error);
20
+ }
21
+ }
@@ -0,0 +1,39 @@
1
+ const DEFAULT_RETRY = {
2
+ maxRetries: 3,
3
+ baseDelayMs: 500,
4
+ maxDelayMs: 30000
5
+ };
6
+ export async function fetchWithRetry(input, init, config = {}) {
7
+ const cfg = { ...DEFAULT_RETRY, ...config };
8
+ let lastResponse;
9
+ for (let attempt = 0;attempt <= cfg.maxRetries; attempt++) {
10
+ lastResponse = await fetch(input, init);
11
+ if (lastResponse.ok)
12
+ return lastResponse;
13
+ if (lastResponse.status < 500 && lastResponse.status !== 429)
14
+ return lastResponse;
15
+ if (attempt === cfg.maxRetries)
16
+ return lastResponse;
17
+ const backoff = parseRetryAfter(lastResponse.headers.get("Retry-After")) ?? exponentialBackoff(attempt, cfg), delay = Math.min(backoff, cfg.maxDelayMs);
18
+ try {
19
+ await lastResponse.text().catch(() => {});
20
+ } catch {}
21
+ await new Promise((resolve) => setTimeout(resolve, delay));
22
+ }
23
+ return lastResponse;
24
+ }
25
+ function parseRetryAfter(header) {
26
+ if (!header)
27
+ return null;
28
+ const seconds = Number.parseInt(header, 10);
29
+ if (Number.isFinite(seconds) && String(seconds) === header.trim())
30
+ return Math.max(0, seconds * 1000);
31
+ const dateMs = Date.parse(header);
32
+ if (Number.isFinite(dateMs))
33
+ return Math.max(0, dateMs - Date.now());
34
+ return null;
35
+ }
36
+ function exponentialBackoff(attempt, cfg) {
37
+ const cap = Math.min(cfg.baseDelayMs * 2 ** attempt, cfg.maxDelayMs);
38
+ return Math.random() * cap;
39
+ }
@@ -0,0 +1,59 @@
1
+ function charsPerToken(model) {
2
+ const m = model.toLowerCase();
3
+ if (m.startsWith("gpt-"))
4
+ return 3.5;
5
+ if (m.startsWith("claude"))
6
+ return 3.5;
7
+ return 3.5;
8
+ }
9
+ export function estimateTokens(text, model = "gpt-4o") {
10
+ if (!text)
11
+ return 0;
12
+ const ratio = charsPerToken(model);
13
+ return Math.max(1, Math.ceil(text.length / ratio));
14
+ }
15
+ export function estimateMessageTokens(messages, model = "gpt-4o") {
16
+ const PER_MESSAGE_OVERHEAD = 4;
17
+ let total = 2;
18
+ for (const msg of messages) {
19
+ total += PER_MESSAGE_OVERHEAD;
20
+ if (typeof msg.content === "string")
21
+ total += estimateTokens(msg.content, model);
22
+ else if (Array.isArray(msg.content)) {
23
+ for (const block of msg.content)
24
+ if (block.type === "text" && block.text)
25
+ total += estimateTokens(block.text, model);
26
+ else if (block.type === "image" || block.type === "image_url")
27
+ total += 100;
28
+ }
29
+ }
30
+ return total;
31
+ }
32
+ const INJECTION_PATTERNS = [
33
+ /\bignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions?\b/i,
34
+ /\bdisregard\s+(?:all\s+)?(?:previous|prior|above)\b/i,
35
+ /\bforget\s+(?:everything|all)\s+(?:you|i)\b/i,
36
+ /\byou\s+are\s+now\s+a\s+\w+/i,
37
+ /\bnew\s+instructions?:\s*/i,
38
+ /\bsystem\s*[:>]\s*/i,
39
+ /\b(?:reveal|show|print|output|display)\s+(?:your|the)\s+(?:system\s+)?prompt\b/i,
40
+ /<\s*\/?\s*system\s*>/i,
41
+ /\[INST\]|\[\/INST\]/i,
42
+ /^\s*###\s+(?:instruction|system)/im
43
+ ];
44
+ export function sanitizePrompt(text) {
45
+ if (!text)
46
+ return { ok: !0, matched: [], cleaned: text };
47
+ const matched = [];
48
+ let cleaned = text;
49
+ for (const pattern of INJECTION_PATTERNS)
50
+ if (pattern.test(cleaned)) {
51
+ matched.push(pattern.toString());
52
+ cleaned = cleaned.replace(pattern, "[redacted]");
53
+ }
54
+ return {
55
+ ok: matched.length === 0,
56
+ matched,
57
+ cleaned
58
+ };
59
+ }
@@ -0,0 +1,27 @@
1
+ const reporters = [];
2
+ export function onUsage(reporter) {
3
+ reporters.push(reporter);
4
+ return () => {
5
+ const idx = reporters.indexOf(reporter);
6
+ if (idx >= 0)
7
+ reporters.splice(idx, 1);
8
+ };
9
+ }
10
+ export function clearUsageReporters() {
11
+ reporters.length = 0;
12
+ }
13
+ export function recordUsage(record) {
14
+ for (const reporter of reporters)
15
+ try {
16
+ const result = reporter(record);
17
+ if (result && typeof result.then === "function")
18
+ result.catch((err) => {
19
+ console.error("[ai/usage] reporter rejected:", err);
20
+ });
21
+ } catch (err) {
22
+ console.error("[ai/usage] reporter threw:", err);
23
+ }
24
+ }
25
+ export function listUsageReporters() {
26
+ return reporters;
27
+ }
@@ -0,0 +1,54 @@
1
+ function toOpenAIContent(block) {
2
+ if (block.type === "text")
3
+ return { type: "text", text: block.text ?? "" };
4
+ if (block.type === "image_url")
5
+ return { type: "image_url", image_url: block.image_url };
6
+ if (block.type === "image" && block.source) {
7
+ if (block.source.type === "base64")
8
+ return { type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } };
9
+ }
10
+ return block;
11
+ }
12
+ function toAnthropicContent(block) {
13
+ if (block.type === "text")
14
+ return { type: "text", text: block.text ?? "" };
15
+ if (block.type === "image" && block.source)
16
+ return { type: "image", source: block.source };
17
+ if (block.type === "image_url" && block.image_url) {
18
+ const url = block.image_url.url, dataMatch = url.match(/^data:([^;]+);base64,(.+)$/);
19
+ if (dataMatch)
20
+ return {
21
+ type: "image",
22
+ source: { type: "base64", media_type: dataMatch[1], data: dataMatch[2] }
23
+ };
24
+ return { type: "image", source: { type: "url", url } };
25
+ }
26
+ return block;
27
+ }
28
+ export function normalizeMessagesForProvider(messages, provider) {
29
+ return messages.map((msg) => {
30
+ if (typeof msg.content === "string")
31
+ return msg;
32
+ const mapper = provider === "openai" ? toOpenAIContent : toAnthropicContent;
33
+ return {
34
+ role: msg.role,
35
+ content: msg.content.map(mapper)
36
+ };
37
+ });
38
+ }
39
+ export function buildMessageWithImages(command, images) {
40
+ const blocks = [];
41
+ for (const img of images)
42
+ if (img.dataBase64 && img.mediaType)
43
+ blocks.push({
44
+ type: "image",
45
+ source: { type: "base64", media_type: img.mediaType, data: img.dataBase64 }
46
+ });
47
+ else if (img.url)
48
+ blocks.push({
49
+ type: "image_url",
50
+ image_url: { url: img.url, detail: img.detail }
51
+ });
52
+ blocks.push({ type: "text", text: command });
53
+ return blocks;
54
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/ai",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.87",
5
+ "version": "0.70.90",
6
6
  "description": "Stacks Artificial Intelligence.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [