@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/package.json +3 -2
- package/src/agents/claude/index.ts +310 -0
- package/src/agents/index.ts +7 -0
- package/src/buddy.ts +619 -0
- package/src/drivers/anthropic/index.ts +430 -0
- package/src/drivers/claude-agent-sdk/index.ts +370 -0
- package/src/drivers/index.ts +14 -0
- package/src/drivers/ollama/index.ts +514 -0
- package/src/drivers/openai/index.ts +529 -0
- package/src/image.ts +607 -0
- package/src/index.ts +53 -0
- package/src/mcp.ts +658 -0
- package/src/personalization.ts +490 -0
- package/src/search.ts +555 -0
- package/src/text.ts +79 -0
- package/src/types.ts +229 -0
- package/src/utils/client-bedrock-runtime.ts +51 -0
- package/src/utils/client-bedrock.ts +75 -0
- package/src/utils/model-access.ts +32 -0
- package/src/utils/retry.ts +124 -0
- package/src/utils/tokens.ts +159 -0
- package/src/utils/usage.ts +98 -0
- package/src/utils/vision.ts +119 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama API Driver
|
|
3
|
+
*
|
|
4
|
+
* Local LLM integration via Ollama.
|
|
5
|
+
* Supports chat completions, streaming, and embeddings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AIDriver, AIDriverConfig, AIMessage, AIResult, ChatCompletionOptions, OllamaAPIResponse } from '../../types'
|
|
9
|
+
|
|
10
|
+
export interface OllamaDriverConfig extends AIDriverConfig {
|
|
11
|
+
host?: string
|
|
12
|
+
model?: string
|
|
13
|
+
embeddingModel?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_HOST = 'http://localhost:11434'
|
|
17
|
+
const DEFAULT_MODEL = 'llama3.2'
|
|
18
|
+
const DEFAULT_EMBEDDING_MODEL = 'nomic-embed-text'
|
|
19
|
+
|
|
20
|
+
let globalConfig: OllamaDriverConfig = {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configure Ollama globally
|
|
24
|
+
*/
|
|
25
|
+
export function configure(config: OllamaDriverConfig): void {
|
|
26
|
+
globalConfig = { ...globalConfig, ...config }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getConfig(config?: Partial<OllamaDriverConfig>): OllamaDriverConfig {
|
|
30
|
+
return {
|
|
31
|
+
host: config?.host || globalConfig.host || process.env.OLLAMA_HOST || DEFAULT_HOST,
|
|
32
|
+
model: config?.model || globalConfig.model || process.env.OLLAMA_MODEL || DEFAULT_MODEL,
|
|
33
|
+
embeddingModel: config?.embeddingModel || globalConfig.embeddingModel || DEFAULT_EMBEDDING_MODEL,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createOllamaDriver(config: OllamaDriverConfig = {}): AIDriver {
|
|
38
|
+
const {
|
|
39
|
+
host = process.env.OLLAMA_HOST || DEFAULT_HOST,
|
|
40
|
+
model = process.env.OLLAMA_MODEL || DEFAULT_MODEL,
|
|
41
|
+
embeddingModel = DEFAULT_EMBEDDING_MODEL,
|
|
42
|
+
} = config
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: 'Ollama',
|
|
46
|
+
|
|
47
|
+
async process(command: string, systemPrompt: string, history: AIMessage[]): Promise<string> {
|
|
48
|
+
const response = await fetch(`${host}/api/chat`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model,
|
|
53
|
+
messages: [
|
|
54
|
+
{ role: 'system', content: systemPrompt },
|
|
55
|
+
...history,
|
|
56
|
+
{ role: 'user', content: command },
|
|
57
|
+
],
|
|
58
|
+
stream: false,
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = await response.text()
|
|
64
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = (await response.json()) as OllamaAPIResponse
|
|
68
|
+
return data.message.content
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async *stream(command: string, systemPrompt: string, history: AIMessage[]): AsyncGenerator<string> {
|
|
72
|
+
const response = await fetch(`${host}/api/chat`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
model,
|
|
77
|
+
messages: [
|
|
78
|
+
{ role: 'system', content: systemPrompt },
|
|
79
|
+
...history,
|
|
80
|
+
{ role: 'user', content: command },
|
|
81
|
+
],
|
|
82
|
+
stream: true,
|
|
83
|
+
}),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const error = await response.text()
|
|
88
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const reader = response.body?.getReader()
|
|
92
|
+
if (!reader) throw new Error('No response body')
|
|
93
|
+
|
|
94
|
+
const decoder = new TextDecoder()
|
|
95
|
+
let buffer = ''
|
|
96
|
+
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read()
|
|
99
|
+
if (done) break
|
|
100
|
+
|
|
101
|
+
buffer += decoder.decode(value, { stream: true })
|
|
102
|
+
const lines = buffer.split('\n')
|
|
103
|
+
buffer = lines.pop() || ''
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (!line.trim()) continue
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(line) as OllamaAPIResponse
|
|
110
|
+
if (data.message?.content) {
|
|
111
|
+
yield data.message.content
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Skip invalid JSON
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async embed(input: string | string[]): Promise<number[] | number[][]> {
|
|
122
|
+
const inputs = Array.isArray(input) ? input : [input]
|
|
123
|
+
const embeddings: number[][] = []
|
|
124
|
+
|
|
125
|
+
for (const text of inputs) {
|
|
126
|
+
const response = await fetch(`${host}/api/embeddings`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
model: embeddingModel,
|
|
131
|
+
prompt: text,
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const error = await response.text()
|
|
137
|
+
throw new Error(`Ollama Embeddings API error: ${error}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = (await response.json()) as { embedding: number[] }
|
|
141
|
+
embeddings.push(data.embedding)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Array.isArray(input) ? embeddings : embeddings[0]
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Chat completion with full options
|
|
151
|
+
*/
|
|
152
|
+
export async function chat(
|
|
153
|
+
messages: AIMessage[],
|
|
154
|
+
options: ChatCompletionOptions = {},
|
|
155
|
+
): Promise<AIResult> {
|
|
156
|
+
const config = getConfig()
|
|
157
|
+
const {
|
|
158
|
+
model = config.model,
|
|
159
|
+
temperature,
|
|
160
|
+
topP,
|
|
161
|
+
stop,
|
|
162
|
+
} = options
|
|
163
|
+
|
|
164
|
+
const response = await fetch(`${config.host}/api/chat`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
model,
|
|
169
|
+
messages,
|
|
170
|
+
stream: false,
|
|
171
|
+
options: {
|
|
172
|
+
temperature,
|
|
173
|
+
top_p: topP,
|
|
174
|
+
stop,
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const error = await response.text()
|
|
181
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const data = (await response.json()) as any
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
content: data.message.content,
|
|
188
|
+
model: data.model,
|
|
189
|
+
usage: {
|
|
190
|
+
promptTokens: data.prompt_eval_count || 0,
|
|
191
|
+
completionTokens: data.eval_count || 0,
|
|
192
|
+
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0),
|
|
193
|
+
},
|
|
194
|
+
finishReason: data.done_reason || 'stop',
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Stream chat completion
|
|
200
|
+
*/
|
|
201
|
+
export async function* streamChat(
|
|
202
|
+
messages: AIMessage[],
|
|
203
|
+
options: ChatCompletionOptions = {},
|
|
204
|
+
): AsyncGenerator<string> {
|
|
205
|
+
const config = getConfig()
|
|
206
|
+
const {
|
|
207
|
+
model = config.model,
|
|
208
|
+
temperature,
|
|
209
|
+
topP,
|
|
210
|
+
stop,
|
|
211
|
+
} = options
|
|
212
|
+
|
|
213
|
+
const response = await fetch(`${config.host}/api/chat`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model,
|
|
218
|
+
messages,
|
|
219
|
+
stream: true,
|
|
220
|
+
options: {
|
|
221
|
+
temperature,
|
|
222
|
+
top_p: topP,
|
|
223
|
+
stop,
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
const error = await response.text()
|
|
230
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const reader = response.body?.getReader()
|
|
234
|
+
if (!reader) throw new Error('No response body')
|
|
235
|
+
|
|
236
|
+
const decoder = new TextDecoder()
|
|
237
|
+
let buffer = ''
|
|
238
|
+
|
|
239
|
+
while (true) {
|
|
240
|
+
const { done, value } = await reader.read()
|
|
241
|
+
if (done) break
|
|
242
|
+
|
|
243
|
+
buffer += decoder.decode(value, { stream: true })
|
|
244
|
+
const lines = buffer.split('\n')
|
|
245
|
+
buffer = lines.pop() || ''
|
|
246
|
+
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
if (!line.trim()) continue
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const data = JSON.parse(line) as OllamaAPIResponse
|
|
252
|
+
if (data.message?.content) {
|
|
253
|
+
yield data.message.content
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Skip invalid JSON
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generate text completion (non-chat)
|
|
265
|
+
*/
|
|
266
|
+
export async function generate(
|
|
267
|
+
prompt: string,
|
|
268
|
+
options: {
|
|
269
|
+
model?: string
|
|
270
|
+
system?: string
|
|
271
|
+
template?: string
|
|
272
|
+
context?: number[]
|
|
273
|
+
raw?: boolean
|
|
274
|
+
format?: 'json'
|
|
275
|
+
images?: string[]
|
|
276
|
+
} = {},
|
|
277
|
+
): Promise<AIResult> {
|
|
278
|
+
const config = getConfig()
|
|
279
|
+
|
|
280
|
+
const response = await fetch(`${config.host}/api/generate`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
model: options.model || config.model,
|
|
285
|
+
prompt,
|
|
286
|
+
system: options.system,
|
|
287
|
+
template: options.template,
|
|
288
|
+
context: options.context,
|
|
289
|
+
raw: options.raw,
|
|
290
|
+
format: options.format,
|
|
291
|
+
images: options.images,
|
|
292
|
+
stream: false,
|
|
293
|
+
}),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
const error = await response.text()
|
|
298
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const data = (await response.json()) as any
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
content: data.response,
|
|
305
|
+
model: data.model,
|
|
306
|
+
usage: {
|
|
307
|
+
promptTokens: data.prompt_eval_count || 0,
|
|
308
|
+
completionTokens: data.eval_count || 0,
|
|
309
|
+
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0),
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create embeddings
|
|
316
|
+
*/
|
|
317
|
+
export async function embed(
|
|
318
|
+
input: string | string[],
|
|
319
|
+
model?: string,
|
|
320
|
+
): Promise<number[] | number[][]> {
|
|
321
|
+
const config = getConfig()
|
|
322
|
+
const embeddingModel = model || config.embeddingModel
|
|
323
|
+
|
|
324
|
+
const inputs = Array.isArray(input) ? input : [input]
|
|
325
|
+
const embeddings: number[][] = []
|
|
326
|
+
|
|
327
|
+
for (const text of inputs) {
|
|
328
|
+
const response = await fetch(`${config.host}/api/embeddings`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
model: embeddingModel,
|
|
333
|
+
prompt: text,
|
|
334
|
+
}),
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
const error = await response.text()
|
|
339
|
+
throw new Error(`Ollama Embeddings API error: ${error}`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const data = (await response.json()) as { embedding: number[] }
|
|
343
|
+
embeddings.push(data.embedding)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return Array.isArray(input) ? embeddings : embeddings[0]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* List available models
|
|
351
|
+
*/
|
|
352
|
+
export async function listModels(): Promise<Array<{
|
|
353
|
+
name: string
|
|
354
|
+
modified_at: string
|
|
355
|
+
size: number
|
|
356
|
+
digest: string
|
|
357
|
+
details: {
|
|
358
|
+
format: string
|
|
359
|
+
family: string
|
|
360
|
+
families: string[]
|
|
361
|
+
parameter_size: string
|
|
362
|
+
quantization_level: string
|
|
363
|
+
}
|
|
364
|
+
}>> {
|
|
365
|
+
const config = getConfig()
|
|
366
|
+
|
|
367
|
+
const response = await fetch(`${config.host}/api/tags`, {
|
|
368
|
+
method: 'GET',
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const error = await response.text()
|
|
373
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const data = (await response.json()) as { models: any[] }
|
|
377
|
+
return data.models
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Pull a model from the library
|
|
382
|
+
*/
|
|
383
|
+
export async function pullModel(
|
|
384
|
+
name: string,
|
|
385
|
+
onProgress?: (status: string, completed?: number, total?: number) => void,
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
const config = getConfig()
|
|
388
|
+
|
|
389
|
+
const response = await fetch(`${config.host}/api/pull`, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers: { 'Content-Type': 'application/json' },
|
|
392
|
+
body: JSON.stringify({ name, stream: true }),
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
if (!response.ok) {
|
|
396
|
+
const error = await response.text()
|
|
397
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const reader = response.body?.getReader()
|
|
401
|
+
if (!reader) return
|
|
402
|
+
|
|
403
|
+
const decoder = new TextDecoder()
|
|
404
|
+
let buffer = ''
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
const { done, value } = await reader.read()
|
|
408
|
+
if (done) break
|
|
409
|
+
|
|
410
|
+
buffer += decoder.decode(value, { stream: true })
|
|
411
|
+
const lines = buffer.split('\n')
|
|
412
|
+
buffer = lines.pop() || ''
|
|
413
|
+
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
if (!line.trim()) continue
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const data = JSON.parse(line) as { status?: string; completed?: number; total?: number }
|
|
419
|
+
if (onProgress) {
|
|
420
|
+
onProgress(data.status, data.completed, data.total)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// Skip invalid JSON
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Delete a model
|
|
432
|
+
*/
|
|
433
|
+
export async function deleteModel(name: string): Promise<void> {
|
|
434
|
+
const config = getConfig()
|
|
435
|
+
|
|
436
|
+
const response = await fetch(`${config.host}/api/delete`, {
|
|
437
|
+
method: 'DELETE',
|
|
438
|
+
headers: { 'Content-Type': 'application/json' },
|
|
439
|
+
body: JSON.stringify({ name }),
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const error = await response.text()
|
|
444
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Show model information
|
|
450
|
+
*/
|
|
451
|
+
export async function showModel(_name: string): Promise<{
|
|
452
|
+
modelfile: string
|
|
453
|
+
parameters: string
|
|
454
|
+
template: string
|
|
455
|
+
details: {
|
|
456
|
+
format: string
|
|
457
|
+
family: string
|
|
458
|
+
families: string[]
|
|
459
|
+
parameter_size: string
|
|
460
|
+
quantization_level: string
|
|
461
|
+
}
|
|
462
|
+
}> {
|
|
463
|
+
const config = getConfig()
|
|
464
|
+
|
|
465
|
+
const response = await fetch(`${config.host}/api/show`, {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: { 'Content-Type': 'application/json' },
|
|
468
|
+
body: JSON.stringify({ name: _name }),
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
if (!response.ok) {
|
|
472
|
+
const error = await response.text()
|
|
473
|
+
throw new Error(`Ollama API error: ${error}`)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return response.json() as any
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Check if Ollama is running
|
|
481
|
+
*/
|
|
482
|
+
export async function isRunning(): Promise<boolean> {
|
|
483
|
+
const config = getConfig()
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(`${config.host}/api/tags`, {
|
|
487
|
+
method: 'GET',
|
|
488
|
+
})
|
|
489
|
+
return response.ok
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export const ollamaDriver: { create: typeof createOllamaDriver } = {
|
|
497
|
+
create: createOllamaDriver,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const ollama = {
|
|
501
|
+
configure,
|
|
502
|
+
chat,
|
|
503
|
+
streamChat,
|
|
504
|
+
generate,
|
|
505
|
+
embed,
|
|
506
|
+
listModels,
|
|
507
|
+
pullModel,
|
|
508
|
+
deleteModel,
|
|
509
|
+
showModel,
|
|
510
|
+
isRunning,
|
|
511
|
+
createDriver: createOllamaDriver,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export default ollama
|