@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.
@@ -0,0 +1,332 @@
1
+ const DEFAULT_HOST = "http://localhost:11434", DEFAULT_MODEL = "llama3.2", DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
2
+ let globalConfig = {};
3
+ export function configure(config) {
4
+ globalConfig = { ...globalConfig, ...config };
5
+ }
6
+ function getConfig(config) {
7
+ return {
8
+ host: config?.host || globalConfig.host || process.env.OLLAMA_HOST || DEFAULT_HOST,
9
+ model: config?.model || globalConfig.model || process.env.OLLAMA_MODEL || DEFAULT_MODEL,
10
+ embeddingModel: config?.embeddingModel || globalConfig.embeddingModel || DEFAULT_EMBEDDING_MODEL
11
+ };
12
+ }
13
+ export function createOllamaDriver(config = {}) {
14
+ const {
15
+ host = process.env.OLLAMA_HOST || DEFAULT_HOST,
16
+ model = process.env.OLLAMA_MODEL || DEFAULT_MODEL,
17
+ embeddingModel = DEFAULT_EMBEDDING_MODEL
18
+ } = config;
19
+ return {
20
+ name: "Ollama",
21
+ async process(command, systemPrompt, history) {
22
+ const response = await fetch(`${host}/api/chat`, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({
26
+ model,
27
+ messages: [
28
+ { role: "system", content: systemPrompt },
29
+ ...history,
30
+ { role: "user", content: command }
31
+ ],
32
+ stream: !1
33
+ })
34
+ });
35
+ if (!response.ok) {
36
+ const error = await response.text();
37
+ throw Error(`Ollama API error: ${error}`);
38
+ }
39
+ return (await response.json()).message.content;
40
+ },
41
+ async* stream(command, systemPrompt, history) {
42
+ const response = await fetch(`${host}/api/chat`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({
46
+ model,
47
+ messages: [
48
+ { role: "system", content: systemPrompt },
49
+ ...history,
50
+ { role: "user", content: command }
51
+ ],
52
+ stream: !0
53
+ })
54
+ });
55
+ if (!response.ok) {
56
+ const error = await response.text();
57
+ throw Error(`Ollama API error: ${error}`);
58
+ }
59
+ const reader = response.body?.getReader();
60
+ if (!reader)
61
+ throw Error("No response body");
62
+ const decoder = new TextDecoder;
63
+ let buffer = "";
64
+ while (!0) {
65
+ const { done, value } = await reader.read();
66
+ if (done)
67
+ break;
68
+ buffer += decoder.decode(value, { stream: !0 });
69
+ const lines = buffer.split(`
70
+ `);
71
+ buffer = lines.pop() || "";
72
+ for (const line of lines) {
73
+ if (!line.trim())
74
+ continue;
75
+ try {
76
+ const data = JSON.parse(line);
77
+ if (data.message?.content)
78
+ yield data.message.content;
79
+ } catch {}
80
+ }
81
+ }
82
+ },
83
+ async embed(input) {
84
+ const inputs = Array.isArray(input) ? input : [input], embeddings = [];
85
+ for (const text of inputs) {
86
+ const response = await fetch(`${host}/api/embeddings`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({
90
+ model: embeddingModel,
91
+ prompt: text
92
+ })
93
+ });
94
+ if (!response.ok) {
95
+ const error = await response.text();
96
+ throw Error(`Ollama Embeddings API error: ${error}`);
97
+ }
98
+ const data = await response.json();
99
+ embeddings.push(data.embedding);
100
+ }
101
+ return Array.isArray(input) ? embeddings : embeddings[0];
102
+ }
103
+ };
104
+ }
105
+ export async function chat(messages, options = {}) {
106
+ const config = getConfig(), {
107
+ model = config.model,
108
+ temperature,
109
+ topP,
110
+ stop
111
+ } = options, response = await fetch(`${config.host}/api/chat`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({
115
+ model,
116
+ messages,
117
+ stream: !1,
118
+ options: {
119
+ temperature,
120
+ top_p: topP,
121
+ stop
122
+ }
123
+ })
124
+ });
125
+ if (!response.ok) {
126
+ const error = await response.text();
127
+ throw Error(`Ollama API error: ${error}`);
128
+ }
129
+ const data = await response.json();
130
+ return {
131
+ content: data.message.content,
132
+ model: data.model,
133
+ usage: {
134
+ promptTokens: data.prompt_eval_count || 0,
135
+ completionTokens: data.eval_count || 0,
136
+ totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
137
+ },
138
+ finishReason: data.done_reason || "stop"
139
+ };
140
+ }
141
+ export async function* streamChat(messages, options = {}) {
142
+ const config = getConfig(), {
143
+ model = config.model,
144
+ temperature,
145
+ topP,
146
+ stop
147
+ } = options, response = await fetch(`${config.host}/api/chat`, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({
151
+ model,
152
+ messages,
153
+ stream: !0,
154
+ options: {
155
+ temperature,
156
+ top_p: topP,
157
+ stop
158
+ }
159
+ })
160
+ });
161
+ if (!response.ok) {
162
+ const error = await response.text();
163
+ throw Error(`Ollama API error: ${error}`);
164
+ }
165
+ const reader = response.body?.getReader();
166
+ if (!reader)
167
+ throw Error("No response body");
168
+ const decoder = new TextDecoder;
169
+ let buffer = "";
170
+ while (!0) {
171
+ const { done, value } = await reader.read();
172
+ if (done)
173
+ break;
174
+ buffer += decoder.decode(value, { stream: !0 });
175
+ const lines = buffer.split(`
176
+ `);
177
+ buffer = lines.pop() || "";
178
+ for (const line of lines) {
179
+ if (!line.trim())
180
+ continue;
181
+ try {
182
+ const data = JSON.parse(line);
183
+ if (data.message?.content)
184
+ yield data.message.content;
185
+ } catch {}
186
+ }
187
+ }
188
+ }
189
+ export async function generate(prompt, options = {}) {
190
+ const config = getConfig(), response = await fetch(`${config.host}/api/generate`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({
194
+ model: options.model || config.model,
195
+ prompt,
196
+ system: options.system,
197
+ template: options.template,
198
+ context: options.context,
199
+ raw: options.raw,
200
+ format: options.format,
201
+ images: options.images,
202
+ stream: !1
203
+ })
204
+ });
205
+ if (!response.ok) {
206
+ const error = await response.text();
207
+ throw Error(`Ollama API error: ${error}`);
208
+ }
209
+ const data = await response.json();
210
+ return {
211
+ content: data.response,
212
+ model: data.model,
213
+ usage: {
214
+ promptTokens: data.prompt_eval_count || 0,
215
+ completionTokens: data.eval_count || 0,
216
+ totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
217
+ }
218
+ };
219
+ }
220
+ export async function embed(input, model) {
221
+ const config = getConfig(), embeddingModel = model || config.embeddingModel, inputs = Array.isArray(input) ? input : [input], embeddings = [];
222
+ for (const text of inputs) {
223
+ const response = await fetch(`${config.host}/api/embeddings`, {
224
+ method: "POST",
225
+ headers: { "Content-Type": "application/json" },
226
+ body: JSON.stringify({
227
+ model: embeddingModel,
228
+ prompt: text
229
+ })
230
+ });
231
+ if (!response.ok) {
232
+ const error = await response.text();
233
+ throw Error(`Ollama Embeddings API error: ${error}`);
234
+ }
235
+ const data = await response.json();
236
+ embeddings.push(data.embedding);
237
+ }
238
+ return Array.isArray(input) ? embeddings : embeddings[0];
239
+ }
240
+ export async function listModels() {
241
+ const config = getConfig(), response = await fetch(`${config.host}/api/tags`, {
242
+ method: "GET"
243
+ });
244
+ if (!response.ok) {
245
+ const error = await response.text();
246
+ throw Error(`Ollama API error: ${error}`);
247
+ }
248
+ return (await response.json()).models;
249
+ }
250
+ export async function pullModel(name, onProgress) {
251
+ const config = getConfig(), response = await fetch(`${config.host}/api/pull`, {
252
+ method: "POST",
253
+ headers: { "Content-Type": "application/json" },
254
+ body: JSON.stringify({ name, stream: !0 })
255
+ });
256
+ if (!response.ok) {
257
+ const error = await response.text();
258
+ throw Error(`Ollama API error: ${error}`);
259
+ }
260
+ const reader = response.body?.getReader();
261
+ if (!reader)
262
+ return;
263
+ const decoder = new TextDecoder;
264
+ let buffer = "";
265
+ while (!0) {
266
+ const { done, value } = await reader.read();
267
+ if (done)
268
+ break;
269
+ buffer += decoder.decode(value, { stream: !0 });
270
+ const lines = buffer.split(`
271
+ `);
272
+ buffer = lines.pop() || "";
273
+ for (const line of lines) {
274
+ if (!line.trim())
275
+ continue;
276
+ try {
277
+ const data = JSON.parse(line);
278
+ if (onProgress)
279
+ onProgress(data.status ?? "", data.completed, data.total);
280
+ } catch {}
281
+ }
282
+ }
283
+ }
284
+ export async function deleteModel(name) {
285
+ const config = getConfig(), response = await fetch(`${config.host}/api/delete`, {
286
+ method: "DELETE",
287
+ headers: { "Content-Type": "application/json" },
288
+ body: JSON.stringify({ name })
289
+ });
290
+ if (!response.ok) {
291
+ const error = await response.text();
292
+ throw Error(`Ollama API error: ${error}`);
293
+ }
294
+ }
295
+ export async function showModel(_name) {
296
+ const config = getConfig(), response = await fetch(`${config.host}/api/show`, {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({ name: _name })
300
+ });
301
+ if (!response.ok) {
302
+ const error = await response.text();
303
+ throw Error(`Ollama API error: ${error}`);
304
+ }
305
+ return response.json();
306
+ }
307
+ export async function isRunning() {
308
+ const config = getConfig();
309
+ try {
310
+ return (await fetch(`${config.host}/api/tags`, {
311
+ method: "GET"
312
+ })).ok;
313
+ } catch {
314
+ return !1;
315
+ }
316
+ }
317
+ export const ollamaDriver = {
318
+ create: createOllamaDriver
319
+ }, ollama = {
320
+ configure,
321
+ chat,
322
+ streamChat,
323
+ generate,
324
+ embed,
325
+ listModels,
326
+ pullModel,
327
+ deleteModel,
328
+ showModel,
329
+ isRunning,
330
+ createDriver: createOllamaDriver
331
+ };
332
+ export default ollama;
@@ -0,0 +1,351 @@
1
+ import { fetchWithRetry } from "../../utils/retry";
2
+ import { recordUsage } from "../../utils/usage";
3
+ import { normalizeMessagesForProvider } from "../../utils/vision";
4
+ const DEFAULT_MODEL = "gpt-4o", DEFAULT_MAX_TOKENS = 4096, DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small", DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
+ let globalConfig = null;
6
+ export function configure(config) {
7
+ globalConfig = config;
8
+ }
9
+ function getConfig(config) {
10
+ const merged = { ...globalConfig, ...config };
11
+ if (!merged.apiKey)
12
+ merged.apiKey = process.env.OPENAI_API_KEY || "";
13
+ return merged;
14
+ }
15
+ export function createOpenAIDriver(config) {
16
+ const {
17
+ apiKey,
18
+ model = DEFAULT_MODEL,
19
+ maxTokens = DEFAULT_MAX_TOKENS,
20
+ baseUrl = DEFAULT_BASE_URL,
21
+ embeddingModel = DEFAULT_EMBEDDING_MODEL
22
+ } = config;
23
+ return {
24
+ name: "OpenAI",
25
+ async process(command, systemPrompt, history) {
26
+ if (!apiKey)
27
+ throw Error("OpenAI API key not set. Configure your API key in settings.");
28
+ const response = await fetchWithRetry(`${baseUrl}/chat/completions`, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${apiKey}`
33
+ },
34
+ body: JSON.stringify({
35
+ model,
36
+ max_tokens: maxTokens,
37
+ messages: [
38
+ { role: "system", content: systemPrompt },
39
+ ...history,
40
+ { role: "user", content: command }
41
+ ]
42
+ })
43
+ });
44
+ if (!response.ok) {
45
+ const error = await response.text();
46
+ throw Error(`OpenAI API error: ${error}`);
47
+ }
48
+ const data = await response.json();
49
+ if (!data.choices || data.choices.length === 0)
50
+ throw Error("OpenAI API returned empty choices");
51
+ return data.choices[0].message.content;
52
+ },
53
+ async* stream(command, systemPrompt, history) {
54
+ if (!apiKey)
55
+ throw Error("OpenAI API key not set. Configure your API key in settings.");
56
+ const response = await fetch(`${baseUrl}/chat/completions`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Authorization: `Bearer ${apiKey}`
61
+ },
62
+ body: JSON.stringify({
63
+ model,
64
+ max_tokens: maxTokens,
65
+ stream: !0,
66
+ messages: [
67
+ { role: "system", content: systemPrompt },
68
+ ...history,
69
+ { role: "user", content: command }
70
+ ]
71
+ })
72
+ });
73
+ if (!response.ok) {
74
+ const error = await response.text();
75
+ throw Error(`OpenAI API error: ${error}`);
76
+ }
77
+ const reader = response.body?.getReader();
78
+ if (!reader)
79
+ throw Error("No response body");
80
+ const decoder = new TextDecoder;
81
+ let buffer = "";
82
+ const handlePayload = function* (data) {
83
+ if (data === "[DONE]")
84
+ return;
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(data);
88
+ } catch {
89
+ return;
90
+ }
91
+ if (parsed?.error) {
92
+ const msg = parsed.error.message || JSON.stringify(parsed.error);
93
+ throw Error(`[openai/stream] mid-stream error: ${msg}`);
94
+ }
95
+ const content = parsed.choices?.[0]?.delta?.content;
96
+ if (content)
97
+ yield content;
98
+ };
99
+ while (!0) {
100
+ const { done, value } = await reader.read();
101
+ if (done)
102
+ break;
103
+ buffer += decoder.decode(value, { stream: !0 });
104
+ const lines = buffer.split(`
105
+ `);
106
+ buffer = lines.pop() || "";
107
+ for (const line of lines)
108
+ if (line.startsWith("data: "))
109
+ yield* handlePayload(line.slice(6));
110
+ }
111
+ if (buffer.startsWith("data: "))
112
+ yield* handlePayload(buffer.slice(6));
113
+ },
114
+ async embed(input) {
115
+ if (!apiKey)
116
+ throw Error("OpenAI API key not set. Configure your API key in settings.");
117
+ const response = await fetch(`${baseUrl}/embeddings`, {
118
+ method: "POST",
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ Authorization: `Bearer ${apiKey}`
122
+ },
123
+ body: JSON.stringify({
124
+ model: embeddingModel,
125
+ input
126
+ })
127
+ });
128
+ if (!response.ok) {
129
+ const error = await response.text();
130
+ throw Error(`OpenAI Embeddings API error: ${error}`);
131
+ }
132
+ const data = await response.json();
133
+ if (Array.isArray(input))
134
+ return data.data.map((d) => d.embedding);
135
+ return data.data[0].embedding;
136
+ }
137
+ };
138
+ }
139
+ export async function chat(messages, options = {}) {
140
+ const config = getConfig(), {
141
+ model = DEFAULT_MODEL,
142
+ maxTokens = DEFAULT_MAX_TOKENS,
143
+ temperature,
144
+ topP,
145
+ stop
146
+ } = options, normalizedMessages = normalizeMessagesForProvider(messages, "openai"), startedAt = Date.now(), response = await fetchWithRetry(`${config.baseUrl || DEFAULT_BASE_URL}/chat/completions`, {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ Authorization: `Bearer ${config.apiKey}`
151
+ },
152
+ body: JSON.stringify({
153
+ model,
154
+ max_tokens: maxTokens,
155
+ temperature,
156
+ top_p: topP,
157
+ stop,
158
+ messages: normalizedMessages
159
+ })
160
+ });
161
+ if (!response.ok) {
162
+ const error = await response.text();
163
+ throw Error(`OpenAI API error: ${error}`);
164
+ }
165
+ const data = await response.json();
166
+ if (!data.choices || data.choices.length === 0)
167
+ throw Error("OpenAI API returned empty choices");
168
+ const result = {
169
+ content: data.choices[0].message.content,
170
+ model: data.model,
171
+ usage: {
172
+ promptTokens: data.usage?.prompt_tokens || 0,
173
+ completionTokens: data.usage?.completion_tokens || 0,
174
+ totalTokens: data.usage?.total_tokens || 0
175
+ },
176
+ finishReason: data.choices[0].finish_reason
177
+ };
178
+ recordUsage({
179
+ provider: "openai",
180
+ model: data.model,
181
+ promptTokens: result.usage.promptTokens,
182
+ completionTokens: result.usage.completionTokens,
183
+ totalTokens: result.usage.totalTokens,
184
+ durationMs: Date.now() - startedAt,
185
+ timestamp: Date.now()
186
+ });
187
+ return result;
188
+ }
189
+ export async function* streamChat(messages, options = {}) {
190
+ const config = getConfig(), {
191
+ model = DEFAULT_MODEL,
192
+ maxTokens = DEFAULT_MAX_TOKENS,
193
+ temperature,
194
+ topP,
195
+ stop
196
+ } = options, response = await fetch(`${config.baseUrl || DEFAULT_BASE_URL}/chat/completions`, {
197
+ method: "POST",
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ Authorization: `Bearer ${config.apiKey}`
201
+ },
202
+ body: JSON.stringify({
203
+ model,
204
+ max_tokens: maxTokens,
205
+ temperature,
206
+ top_p: topP,
207
+ stop,
208
+ stream: !0,
209
+ messages: normalizeMessagesForProvider(messages, "openai")
210
+ })
211
+ });
212
+ if (!response.ok) {
213
+ const error = await response.text();
214
+ throw Error(`OpenAI API error: ${error}`);
215
+ }
216
+ const reader = response.body?.getReader();
217
+ if (!reader)
218
+ throw Error("No response body");
219
+ const decoder = new TextDecoder;
220
+ let buffer = "";
221
+ while (!0) {
222
+ const { done, value } = await reader.read();
223
+ if (done)
224
+ break;
225
+ buffer += decoder.decode(value, { stream: !0 });
226
+ const lines = buffer.split(`
227
+ `);
228
+ buffer = lines.pop() || "";
229
+ for (const line of lines)
230
+ if (line.startsWith("data: ")) {
231
+ const data = line.slice(6);
232
+ if (data === "[DONE]")
233
+ continue;
234
+ try {
235
+ const content = JSON.parse(data).choices[0]?.delta?.content;
236
+ if (content)
237
+ yield content;
238
+ } catch {}
239
+ }
240
+ }
241
+ }
242
+ export async function embed(input, model = DEFAULT_EMBEDDING_MODEL) {
243
+ const config = getConfig(), response = await fetch(`${config.baseUrl || DEFAULT_BASE_URL}/embeddings`, {
244
+ method: "POST",
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ Authorization: `Bearer ${config.apiKey}`
248
+ },
249
+ body: JSON.stringify({ model, input })
250
+ });
251
+ if (!response.ok) {
252
+ const error = await response.text();
253
+ throw Error(`OpenAI Embeddings API error: ${error}`);
254
+ }
255
+ const data = await response.json();
256
+ if (Array.isArray(input))
257
+ return data.data.map((d) => d.embedding);
258
+ return data.data[0].embedding;
259
+ }
260
+ export async function generateImage(prompt, options = {}) {
261
+ const config = getConfig(), {
262
+ model = "dall-e-3",
263
+ size = "1024x1024",
264
+ quality = "standard",
265
+ n = 1,
266
+ responseFormat = "url"
267
+ } = options, response = await fetch(`${config.baseUrl || DEFAULT_BASE_URL}/images/generations`, {
268
+ method: "POST",
269
+ headers: {
270
+ "Content-Type": "application/json",
271
+ Authorization: `Bearer ${config.apiKey}`
272
+ },
273
+ body: JSON.stringify({
274
+ model,
275
+ prompt,
276
+ size,
277
+ quality,
278
+ n,
279
+ response_format: responseFormat
280
+ })
281
+ });
282
+ if (!response.ok) {
283
+ const error = await response.text();
284
+ throw Error(`OpenAI Image API error: ${error}`);
285
+ }
286
+ return (await response.json()).data;
287
+ }
288
+ export async function transcribe(audioFile, options = {}) {
289
+ const config = getConfig(), formData = new FormData;
290
+ formData.append("file", audioFile);
291
+ formData.append("model", options.model || "whisper-1");
292
+ if (options.language)
293
+ formData.append("language", options.language);
294
+ if (options.prompt)
295
+ formData.append("prompt", options.prompt);
296
+ if (options.responseFormat)
297
+ formData.append("response_format", options.responseFormat);
298
+ if (options.temperature !== void 0)
299
+ formData.append("temperature", String(options.temperature));
300
+ const response = await fetch(`${config.baseUrl || DEFAULT_BASE_URL}/audio/transcriptions`, {
301
+ method: "POST",
302
+ headers: {
303
+ Authorization: `Bearer ${config.apiKey}`
304
+ },
305
+ body: formData
306
+ });
307
+ if (!response.ok) {
308
+ const error = await response.text();
309
+ throw Error(`OpenAI Whisper API error: ${error}`);
310
+ }
311
+ return response.json();
312
+ }
313
+ export async function textToSpeech(input, options = {}) {
314
+ const config = getConfig(), {
315
+ model = "tts-1",
316
+ voice = "alloy",
317
+ responseFormat = "mp3",
318
+ speed = 1
319
+ } = options, response = await fetch(`${config.baseUrl || DEFAULT_BASE_URL}/audio/speech`, {
320
+ method: "POST",
321
+ headers: {
322
+ "Content-Type": "application/json",
323
+ Authorization: `Bearer ${config.apiKey}`
324
+ },
325
+ body: JSON.stringify({
326
+ model,
327
+ input,
328
+ voice,
329
+ response_format: responseFormat,
330
+ speed
331
+ })
332
+ });
333
+ if (!response.ok) {
334
+ const error = await response.text();
335
+ throw Error(`OpenAI TTS API error: ${error}`);
336
+ }
337
+ return response.arrayBuffer();
338
+ }
339
+ export const openaiDriver = {
340
+ create: createOpenAIDriver
341
+ }, openai = {
342
+ configure,
343
+ chat,
344
+ streamChat,
345
+ embed,
346
+ generateImage,
347
+ transcribe,
348
+ textToSpeech,
349
+ createDriver: createOpenAIDriver
350
+ };
351
+ export default openai;