@happyvertical/ai 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +384 -0
- package/dist/chunks/anthropic-BRwbhwIl.js +463 -0
- package/dist/chunks/anthropic-BRwbhwIl.js.map +1 -0
- package/dist/chunks/bedrock-Cf1xUerN.js +808 -0
- package/dist/chunks/bedrock-Cf1xUerN.js.map +1 -0
- package/dist/chunks/bifrost-3mXtQsTj.js +233 -0
- package/dist/chunks/bifrost-3mXtQsTj.js.map +1 -0
- package/dist/chunks/claude-cli-BrHRfkry.js +603 -0
- package/dist/chunks/claude-cli-BrHRfkry.js.map +1 -0
- package/dist/chunks/gateway-admin-C4GFPbZF.js +359 -0
- package/dist/chunks/gateway-admin-C4GFPbZF.js.map +1 -0
- package/dist/chunks/gemini-BfpHXDIQ.js +662 -0
- package/dist/chunks/gemini-BfpHXDIQ.js.map +1 -0
- package/dist/chunks/huggingface-280qv9iv.js +366 -0
- package/dist/chunks/huggingface-280qv9iv.js.map +1 -0
- package/dist/chunks/index-BT4thAvS.js +934 -0
- package/dist/chunks/index-BT4thAvS.js.map +1 -0
- package/dist/chunks/litellm-DhPKa_Jz.js +220 -0
- package/dist/chunks/litellm-DhPKa_Jz.js.map +1 -0
- package/dist/chunks/ollama-Di1ldur0.js +851 -0
- package/dist/chunks/ollama-Di1ldur0.js.map +1 -0
- package/dist/chunks/openai-5snI2diE.js +749 -0
- package/dist/chunks/openai-5snI2diE.js.map +1 -0
- package/dist/chunks/qwen-tts-DgPgdXxG.js +365 -0
- package/dist/chunks/qwen-tts-DgPgdXxG.js.map +1 -0
- package/dist/chunks/usage-DMWiJ2oB.js +21 -0
- package/dist/chunks/usage-DMWiJ2oB.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/node/factory.d.ts +27 -0
- package/dist/node/factory.d.ts.map +1 -0
- package/dist/shared/client.d.ts +410 -0
- package/dist/shared/client.d.ts.map +1 -0
- package/dist/shared/factory.d.ts +83 -0
- package/dist/shared/factory.d.ts.map +1 -0
- package/dist/shared/message.d.ts +71 -0
- package/dist/shared/message.d.ts.map +1 -0
- package/dist/shared/providers/anthropic.d.ts +82 -0
- package/dist/shared/providers/anthropic.d.ts.map +1 -0
- package/dist/shared/providers/bedrock.d.ts +49 -0
- package/dist/shared/providers/bedrock.d.ts.map +1 -0
- package/dist/shared/providers/bifrost.d.ts +25 -0
- package/dist/shared/providers/bifrost.d.ts.map +1 -0
- package/dist/shared/providers/claude-cli.d.ts +139 -0
- package/dist/shared/providers/claude-cli.d.ts.map +1 -0
- package/dist/shared/providers/gateway-admin.d.ts +35 -0
- package/dist/shared/providers/gateway-admin.d.ts.map +1 -0
- package/dist/shared/providers/gemini.d.ts +116 -0
- package/dist/shared/providers/gemini.d.ts.map +1 -0
- package/dist/shared/providers/huggingface.d.ts +33 -0
- package/dist/shared/providers/huggingface.d.ts.map +1 -0
- package/dist/shared/providers/litellm.d.ts +25 -0
- package/dist/shared/providers/litellm.d.ts.map +1 -0
- package/dist/shared/providers/ollama.d.ts +47 -0
- package/dist/shared/providers/ollama.d.ts.map +1 -0
- package/dist/shared/providers/openai.d.ts +272 -0
- package/dist/shared/providers/openai.d.ts.map +1 -0
- package/dist/shared/providers/qwen-tts.d.ts +85 -0
- package/dist/shared/providers/qwen-tts.d.ts.map +1 -0
- package/dist/shared/providers/usage.d.ts +14 -0
- package/dist/shared/providers/usage.d.ts.map +1 -0
- package/dist/shared/rate-limit.d.ts +13 -0
- package/dist/shared/rate-limit.d.ts.map +1 -0
- package/dist/shared/thread.d.ts +104 -0
- package/dist/shared/thread.d.ts.map +1 -0
- package/dist/shared/types.d.ts +1779 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/metadata.json +35 -0
- package/package.json +62 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { ValidationError } from "@happyvertical/utils";
|
|
2
|
+
import { C as ContentFilterError, a as ContextLengthError, A as AIError, R as RateLimitError, M as ModelNotFoundError, b as AuthenticationError, c as extractTextContent } from "./index-BT4thAvS.js";
|
|
3
|
+
import { e as emitUsage } from "./usage-DMWiJ2oB.js";
|
|
4
|
+
const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
|
|
5
|
+
const OLLAMA_CAPABILITIES = {
|
|
6
|
+
chat: true,
|
|
7
|
+
completion: true,
|
|
8
|
+
embeddings: true,
|
|
9
|
+
streaming: true,
|
|
10
|
+
functions: true,
|
|
11
|
+
vision: true,
|
|
12
|
+
fineTuning: false,
|
|
13
|
+
imageEmbeddings: true,
|
|
14
|
+
imageGeneration: true,
|
|
15
|
+
tts: false,
|
|
16
|
+
voiceCloning: false,
|
|
17
|
+
voiceDesign: false,
|
|
18
|
+
maxContextLength: 131072,
|
|
19
|
+
supportedOperations: [
|
|
20
|
+
"chat",
|
|
21
|
+
"completion",
|
|
22
|
+
"embedding",
|
|
23
|
+
"streaming",
|
|
24
|
+
"functions",
|
|
25
|
+
"vision",
|
|
26
|
+
"image_embedding",
|
|
27
|
+
"image_generation"
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
function stripTrailingSlash(value) {
|
|
31
|
+
return value.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
function normalizeHost(baseUrl) {
|
|
34
|
+
const rawBase = baseUrl?.trim() || DEFAULT_OLLAMA_HOST;
|
|
35
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(rawBase) ? rawBase : `http://${rawBase}`;
|
|
36
|
+
const base = stripTrailingSlash(withScheme);
|
|
37
|
+
if (base.endsWith("/api")) return base.slice(0, -4);
|
|
38
|
+
if (base.endsWith("/v1")) return base.slice(0, -3);
|
|
39
|
+
return base;
|
|
40
|
+
}
|
|
41
|
+
function isEmbeddingModel(modelId) {
|
|
42
|
+
return /(?:^|[-_:/])(?:embed|embedding)|nomic-embed|mxbai-embed/i.test(
|
|
43
|
+
modelId
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
function isImageGenerationModel(modelId) {
|
|
47
|
+
return /(?:^|[-_:/])(flux|sdxl|stable(?:[-_ ]?diffusion)?|z-image|image(?:[-_ ]?(?:turbo|gen|generator)))/i.test(
|
|
48
|
+
modelId
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
function inferVisionSupport(modelId, capabilities) {
|
|
52
|
+
if (capabilities.has("vision")) return true;
|
|
53
|
+
return /vision|llava|bakllava|moondream|pixtral|qwen.*vl|vl-|gemma3/i.test(
|
|
54
|
+
modelId
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function inferFunctionSupport(modelId, capabilities) {
|
|
58
|
+
if (capabilities.has("tools") || capabilities.has("tool_calling") || capabilities.has("function_calling")) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (isEmbeddingModel(modelId) || isImageGenerationModel(modelId)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return /qwen|llama3|gpt-oss|mistral|deepseek|command-r|phi4/i.test(modelId);
|
|
65
|
+
}
|
|
66
|
+
function parseCapabilities(modelId, show) {
|
|
67
|
+
const capabilities = new Set(
|
|
68
|
+
(show?.capabilities || []).map((capability) => capability.toLowerCase())
|
|
69
|
+
);
|
|
70
|
+
if (isImageGenerationModel(modelId)) {
|
|
71
|
+
return ["image_generation"];
|
|
72
|
+
}
|
|
73
|
+
if (capabilities.has("embedding") || capabilities.has("embeddings") || isEmbeddingModel(modelId)) {
|
|
74
|
+
return ["embeddings"];
|
|
75
|
+
}
|
|
76
|
+
const supported = /* @__PURE__ */ new Set(["text", "chat"]);
|
|
77
|
+
if (inferVisionSupport(modelId, capabilities)) {
|
|
78
|
+
supported.add("vision");
|
|
79
|
+
}
|
|
80
|
+
if (inferFunctionSupport(modelId, capabilities)) {
|
|
81
|
+
supported.add("functions");
|
|
82
|
+
}
|
|
83
|
+
return [...supported];
|
|
84
|
+
}
|
|
85
|
+
function parseContextLength(modelId, show) {
|
|
86
|
+
const modelInfo = show?.model_info || {};
|
|
87
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
88
|
+
if (key.endsWith(".context_length") && typeof value === "number") {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const numCtxMatch = show?.parameters?.match(/\bnum_ctx\s+(\d+)/);
|
|
93
|
+
if (numCtxMatch?.[1]) {
|
|
94
|
+
return Number(numCtxMatch[1]);
|
|
95
|
+
}
|
|
96
|
+
if (isEmbeddingModel(modelId)) return 8192;
|
|
97
|
+
if (/gemma3|qwen3|gpt-oss|llama3/i.test(modelId)) return 131072;
|
|
98
|
+
return 32768;
|
|
99
|
+
}
|
|
100
|
+
function mapUsage(promptTokens, completionTokens) {
|
|
101
|
+
if (typeof promptTokens !== "number" && typeof completionTokens !== "number") {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const prompt = promptTokens || 0;
|
|
105
|
+
const completion = completionTokens || 0;
|
|
106
|
+
return {
|
|
107
|
+
promptTokens: prompt,
|
|
108
|
+
completionTokens: completion,
|
|
109
|
+
totalTokens: prompt + completion
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function mapFinishReason(reason) {
|
|
113
|
+
switch (reason) {
|
|
114
|
+
case "stop":
|
|
115
|
+
return "stop";
|
|
116
|
+
case "length":
|
|
117
|
+
return "length";
|
|
118
|
+
case "content_filter":
|
|
119
|
+
return "content_filter";
|
|
120
|
+
default:
|
|
121
|
+
return "stop";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function scoreModelForCapability(model, capability) {
|
|
125
|
+
const id = model.id.toLowerCase();
|
|
126
|
+
let score = 0;
|
|
127
|
+
if (id.includes(":latest")) score += 6;
|
|
128
|
+
if (id.includes("instruct") || id.includes("-it")) score += 6;
|
|
129
|
+
if (id.startsWith("hf.co/")) score -= 8;
|
|
130
|
+
if (/ocr|rerank|embed|image|diffusion|flux/.test(id)) score -= 12;
|
|
131
|
+
switch (capability) {
|
|
132
|
+
case "vision":
|
|
133
|
+
if (model.supportsVision) score += 25;
|
|
134
|
+
if (/gemma4|gemma3/.test(id)) score += 12;
|
|
135
|
+
if (/qwen.*vl|llava|moondream|vision/.test(id)) score += 8;
|
|
136
|
+
if (/ocr/.test(id)) score -= 10;
|
|
137
|
+
break;
|
|
138
|
+
case "embedding":
|
|
139
|
+
if (model.capabilities.includes("embeddings")) score += 20;
|
|
140
|
+
if (/nomic|mxbai|embed/.test(id)) score += 6;
|
|
141
|
+
break;
|
|
142
|
+
case "image_generation":
|
|
143
|
+
if (model.capabilities.includes("image_generation")) score += 20;
|
|
144
|
+
if (/flux|sdxl|stable/.test(id)) score += 8;
|
|
145
|
+
break;
|
|
146
|
+
case "chat":
|
|
147
|
+
default:
|
|
148
|
+
if (model.capabilities.includes("chat")) score += 12;
|
|
149
|
+
if (model.supportsFunctions) score += 4;
|
|
150
|
+
if (/llama|mistral|qwen|gemma|phi|command-r|deepseek-coder/.test(id)) {
|
|
151
|
+
score += 8;
|
|
152
|
+
}
|
|
153
|
+
if (/gpt-oss/.test(id)) score -= 3;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return score;
|
|
157
|
+
}
|
|
158
|
+
class OllamaProvider {
|
|
159
|
+
options;
|
|
160
|
+
host;
|
|
161
|
+
configuredDefaultModel;
|
|
162
|
+
modelListPromise;
|
|
163
|
+
modelShowCache = /* @__PURE__ */ new Map();
|
|
164
|
+
resolvedModelCache = /* @__PURE__ */ new Map();
|
|
165
|
+
constructor(options) {
|
|
166
|
+
this.host = normalizeHost(options.baseUrl);
|
|
167
|
+
this.options = {
|
|
168
|
+
...options,
|
|
169
|
+
baseUrl: this.host
|
|
170
|
+
};
|
|
171
|
+
this.configuredDefaultModel = options.defaultModel;
|
|
172
|
+
}
|
|
173
|
+
get nativeBaseUrl() {
|
|
174
|
+
return `${this.host}/api`;
|
|
175
|
+
}
|
|
176
|
+
get compatibilityBaseUrl() {
|
|
177
|
+
return `${this.host}/v1`;
|
|
178
|
+
}
|
|
179
|
+
buildHeaders(stream = false) {
|
|
180
|
+
const headers = new Headers(this.options.headers);
|
|
181
|
+
headers.set("Content-Type", "application/json");
|
|
182
|
+
headers.set(
|
|
183
|
+
"Accept",
|
|
184
|
+
stream ? "application/x-ndjson, application/json" : "application/json"
|
|
185
|
+
);
|
|
186
|
+
if (this.options.apiKey) {
|
|
187
|
+
headers.set("Authorization", `Bearer ${this.options.apiKey}`);
|
|
188
|
+
}
|
|
189
|
+
return headers;
|
|
190
|
+
}
|
|
191
|
+
async fetchWithTimeout(url, init) {
|
|
192
|
+
const controller = new AbortController();
|
|
193
|
+
const timeout = typeof this.options.timeout === "number" ? this.options.timeout : 0;
|
|
194
|
+
const timer = timeout ? setTimeout(() => controller.abort(), timeout) : void 0;
|
|
195
|
+
try {
|
|
196
|
+
return await fetch(url, {
|
|
197
|
+
...init,
|
|
198
|
+
signal: controller.signal
|
|
199
|
+
});
|
|
200
|
+
} finally {
|
|
201
|
+
if (timer) clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async buildHttpError(response) {
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
const message = text && text.trim().length > 0 ? text.trim() : `Ollama request failed with status ${response.status}`;
|
|
207
|
+
switch (response.status) {
|
|
208
|
+
case 401:
|
|
209
|
+
case 403:
|
|
210
|
+
return new AuthenticationError("ollama");
|
|
211
|
+
case 404:
|
|
212
|
+
return new ModelNotFoundError(message, "ollama");
|
|
213
|
+
case 413:
|
|
214
|
+
return new ContextLengthError("ollama");
|
|
215
|
+
case 429:
|
|
216
|
+
return new RateLimitError("ollama");
|
|
217
|
+
default:
|
|
218
|
+
if (/content[_ -]?filter/i.test(message)) {
|
|
219
|
+
return new ContentFilterError("ollama");
|
|
220
|
+
}
|
|
221
|
+
if (/context|too long|maximum context/i.test(message)) {
|
|
222
|
+
return new ContextLengthError("ollama");
|
|
223
|
+
}
|
|
224
|
+
return new AIError(message, "API_ERROR", "ollama");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
mapError(error) {
|
|
228
|
+
if (error instanceof AIError) {
|
|
229
|
+
return error;
|
|
230
|
+
}
|
|
231
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
232
|
+
return new AIError("Ollama request timed out", "TIMEOUT", "ollama");
|
|
233
|
+
}
|
|
234
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
235
|
+
return new AIError(message, "UNKNOWN_ERROR", "ollama");
|
|
236
|
+
}
|
|
237
|
+
async requestJson(path, body, options = {}) {
|
|
238
|
+
const baseUrl = options.compatibility ? this.compatibilityBaseUrl : this.nativeBaseUrl;
|
|
239
|
+
const response = await this.fetchWithTimeout(`${baseUrl}${path}`, {
|
|
240
|
+
method: body ? "POST" : "GET",
|
|
241
|
+
headers: this.buildHeaders(),
|
|
242
|
+
body: body ? JSON.stringify(body) : void 0
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw await this.buildHttpError(response);
|
|
246
|
+
}
|
|
247
|
+
return await response.json();
|
|
248
|
+
}
|
|
249
|
+
async requestStream(path, body) {
|
|
250
|
+
const response = await this.fetchWithTimeout(
|
|
251
|
+
`${this.nativeBaseUrl}${path}`,
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: this.buildHeaders(true),
|
|
255
|
+
body: JSON.stringify(body)
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw await this.buildHttpError(response);
|
|
260
|
+
}
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
async *parseNdjson(response) {
|
|
264
|
+
const reader = response.body?.getReader();
|
|
265
|
+
if (!reader) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const decoder = new TextDecoder();
|
|
269
|
+
let buffer = "";
|
|
270
|
+
while (true) {
|
|
271
|
+
const { value, done } = await reader.read();
|
|
272
|
+
if (done) break;
|
|
273
|
+
buffer += decoder.decode(value, { stream: true });
|
|
274
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
275
|
+
while (newlineIndex !== -1) {
|
|
276
|
+
const line2 = buffer.slice(0, newlineIndex).trim();
|
|
277
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
278
|
+
if (line2) {
|
|
279
|
+
yield JSON.parse(line2);
|
|
280
|
+
}
|
|
281
|
+
newlineIndex = buffer.indexOf("\n");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
buffer += decoder.decode();
|
|
285
|
+
const line = buffer.trim();
|
|
286
|
+
if (line) {
|
|
287
|
+
yield JSON.parse(line);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async showModel(model) {
|
|
291
|
+
const cached = this.modelShowCache.get(model);
|
|
292
|
+
if (cached) {
|
|
293
|
+
return cached;
|
|
294
|
+
}
|
|
295
|
+
const pending = this.requestJson("/show", {
|
|
296
|
+
model,
|
|
297
|
+
verbose: false
|
|
298
|
+
}).catch((_error) => null);
|
|
299
|
+
this.modelShowCache.set(model, pending);
|
|
300
|
+
return pending;
|
|
301
|
+
}
|
|
302
|
+
async resolveModel(capability, explicitModel) {
|
|
303
|
+
if (explicitModel) {
|
|
304
|
+
return explicitModel;
|
|
305
|
+
}
|
|
306
|
+
if (this.configuredDefaultModel && (capability === "chat" || capability === "vision")) {
|
|
307
|
+
return this.configuredDefaultModel;
|
|
308
|
+
}
|
|
309
|
+
const cached = this.resolvedModelCache.get(capability);
|
|
310
|
+
if (cached) {
|
|
311
|
+
return cached;
|
|
312
|
+
}
|
|
313
|
+
const pending = this.selectModel(capability).catch((error) => {
|
|
314
|
+
this.resolvedModelCache.delete(capability);
|
|
315
|
+
throw error;
|
|
316
|
+
});
|
|
317
|
+
this.resolvedModelCache.set(capability, pending);
|
|
318
|
+
return pending;
|
|
319
|
+
}
|
|
320
|
+
async selectModel(capability) {
|
|
321
|
+
const models = await this.getModels();
|
|
322
|
+
const candidates = models.filter((candidate) => {
|
|
323
|
+
switch (capability) {
|
|
324
|
+
case "vision":
|
|
325
|
+
return candidate.capabilities.includes("vision") || candidate.supportsVision === true;
|
|
326
|
+
case "embedding":
|
|
327
|
+
return candidate.capabilities.includes("embeddings");
|
|
328
|
+
case "image_generation":
|
|
329
|
+
return candidate.capabilities.includes("image_generation");
|
|
330
|
+
case "chat":
|
|
331
|
+
default:
|
|
332
|
+
return candidate.capabilities.includes("chat");
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const model = candidates.sort(
|
|
336
|
+
(left, right) => scoreModelForCapability(right, capability) - scoreModelForCapability(left, capability)
|
|
337
|
+
)[0];
|
|
338
|
+
if (!model) {
|
|
339
|
+
throw new ValidationError(
|
|
340
|
+
`No ${capability} model is available from the Ollama host`,
|
|
341
|
+
{
|
|
342
|
+
provider: "ollama",
|
|
343
|
+
capability,
|
|
344
|
+
hint: "Pass model/defaultModel explicitly or make sure the Ollama host has a compatible model installed"
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return model.id;
|
|
349
|
+
}
|
|
350
|
+
isVisionRequest(messages) {
|
|
351
|
+
return messages.some(
|
|
352
|
+
(message) => Array.isArray(message.content) && message.content.some((part) => part.type === "image_url")
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
async imageToBase64Payload(image) {
|
|
356
|
+
if (Buffer.isBuffer(image)) {
|
|
357
|
+
return image.toString("base64");
|
|
358
|
+
}
|
|
359
|
+
if (image.startsWith("data:")) {
|
|
360
|
+
const commaIndex = image.indexOf(",");
|
|
361
|
+
return commaIndex === -1 ? image : image.slice(commaIndex + 1);
|
|
362
|
+
}
|
|
363
|
+
const response = await fetch(image);
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
throw new AIError(
|
|
366
|
+
`Failed to fetch image: ${response.status} ${response.statusText}`,
|
|
367
|
+
"IMAGE_FETCH_ERROR",
|
|
368
|
+
"ollama"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
372
|
+
return Buffer.from(arrayBuffer).toString("base64");
|
|
373
|
+
}
|
|
374
|
+
async imageToDataUrl(image) {
|
|
375
|
+
if (Buffer.isBuffer(image)) {
|
|
376
|
+
return `data:image/png;base64,${image.toString("base64")}`;
|
|
377
|
+
}
|
|
378
|
+
if (image.startsWith("data:")) {
|
|
379
|
+
return image;
|
|
380
|
+
}
|
|
381
|
+
const response = await fetch(image);
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new AIError(
|
|
384
|
+
`Failed to fetch image: ${response.status} ${response.statusText}`,
|
|
385
|
+
"IMAGE_FETCH_ERROR",
|
|
386
|
+
"ollama"
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const contentType = response.headers.get("content-type") || "image/png";
|
|
390
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
391
|
+
return `data:${contentType};base64,${Buffer.from(arrayBuffer).toString("base64")}`;
|
|
392
|
+
}
|
|
393
|
+
parseToolArguments(argumentsText) {
|
|
394
|
+
if (!argumentsText) {
|
|
395
|
+
return {};
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const parsed = JSON.parse(argumentsText);
|
|
399
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
400
|
+
} catch {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async mapMessagesToOllama(messages) {
|
|
405
|
+
const mappedMessages = await Promise.all(
|
|
406
|
+
messages.map(async (message) => {
|
|
407
|
+
const content = extractTextContent(message.content);
|
|
408
|
+
const mapped = {
|
|
409
|
+
role: message.role === "function" ? "tool" : message.role,
|
|
410
|
+
content
|
|
411
|
+
};
|
|
412
|
+
if (Array.isArray(message.content)) {
|
|
413
|
+
const imageParts = message.content.filter(
|
|
414
|
+
(part) => part.type === "image_url"
|
|
415
|
+
);
|
|
416
|
+
if (imageParts.length > 0) {
|
|
417
|
+
mapped.images = await Promise.all(
|
|
418
|
+
imageParts.map(
|
|
419
|
+
(part) => this.imageToBase64Payload(part.image_url.url)
|
|
420
|
+
)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (mapped.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
425
|
+
mapped.tool_calls = message.tool_calls.map((toolCall, index) => ({
|
|
426
|
+
type: "function",
|
|
427
|
+
function: {
|
|
428
|
+
index,
|
|
429
|
+
name: toolCall.function.name,
|
|
430
|
+
arguments: this.parseToolArguments(toolCall.function.arguments)
|
|
431
|
+
}
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
if (mapped.role === "tool" && message.name) {
|
|
435
|
+
mapped.tool_name = message.name;
|
|
436
|
+
}
|
|
437
|
+
return mapped;
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
return mappedMessages;
|
|
441
|
+
}
|
|
442
|
+
mapTools(tools, toolChoice) {
|
|
443
|
+
if (!tools || tools.length === 0 || toolChoice === "none") {
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
const scopedTools = toolChoice && typeof toolChoice === "object" ? tools.filter(
|
|
447
|
+
(tool) => tool.function.name === toolChoice.function.name
|
|
448
|
+
) : tools;
|
|
449
|
+
if (scopedTools.length === 0) {
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
return scopedTools.map((tool) => ({
|
|
453
|
+
type: "function",
|
|
454
|
+
function: {
|
|
455
|
+
name: tool.function.name,
|
|
456
|
+
description: tool.function.description,
|
|
457
|
+
parameters: tool.function.parameters || { type: "object" }
|
|
458
|
+
}
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
buildRuntimeOptions(options) {
|
|
462
|
+
const runtimeOptions = {};
|
|
463
|
+
if (typeof options.maxTokens === "number") {
|
|
464
|
+
runtimeOptions.num_predict = options.maxTokens;
|
|
465
|
+
}
|
|
466
|
+
if (typeof options.temperature === "number") {
|
|
467
|
+
runtimeOptions.temperature = options.temperature;
|
|
468
|
+
}
|
|
469
|
+
if (typeof options.topP === "number") {
|
|
470
|
+
runtimeOptions.top_p = options.topP;
|
|
471
|
+
}
|
|
472
|
+
if (options.stop) {
|
|
473
|
+
runtimeOptions.stop = Array.isArray(options.stop) ? options.stop : [options.stop];
|
|
474
|
+
}
|
|
475
|
+
if (typeof options.seed === "number") {
|
|
476
|
+
runtimeOptions.seed = options.seed;
|
|
477
|
+
}
|
|
478
|
+
return Object.keys(runtimeOptions).length > 0 ? runtimeOptions : void 0;
|
|
479
|
+
}
|
|
480
|
+
mapThink(options) {
|
|
481
|
+
if (!Object.hasOwn(options, "thinkingLevel")) {
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
return options.thinkingLevel;
|
|
485
|
+
}
|
|
486
|
+
async chat(messages, options = {}) {
|
|
487
|
+
const startTime = Date.now();
|
|
488
|
+
try {
|
|
489
|
+
const model = await this.resolveModel(
|
|
490
|
+
this.isVisionRequest(messages) ? "vision" : "chat",
|
|
491
|
+
options.model
|
|
492
|
+
);
|
|
493
|
+
const response = await this.requestJson("/chat", {
|
|
494
|
+
model,
|
|
495
|
+
messages: await this.mapMessagesToOllama(messages),
|
|
496
|
+
stream: false,
|
|
497
|
+
format: options.responseFormat?.type === "json_object" ? "json" : void 0,
|
|
498
|
+
tools: this.mapTools(options.tools, options.toolChoice),
|
|
499
|
+
think: this.mapThink(options),
|
|
500
|
+
keep_alive: this.options.keepAlive,
|
|
501
|
+
options: this.buildRuntimeOptions(options)
|
|
502
|
+
});
|
|
503
|
+
const usage = mapUsage(response.prompt_eval_count, response.eval_count);
|
|
504
|
+
emitUsage(
|
|
505
|
+
this.options,
|
|
506
|
+
"ollama",
|
|
507
|
+
"chat",
|
|
508
|
+
response.model || model,
|
|
509
|
+
usage,
|
|
510
|
+
startTime,
|
|
511
|
+
options.usageTags
|
|
512
|
+
);
|
|
513
|
+
const toolCalls = response.message?.tool_calls?.filter((call) => call.function?.name).map((call, index) => ({
|
|
514
|
+
id: `${response.model || model}-tool-${index + 1}`,
|
|
515
|
+
type: "function",
|
|
516
|
+
function: {
|
|
517
|
+
name: call.function?.name || "",
|
|
518
|
+
arguments: JSON.stringify(call.function?.arguments || {})
|
|
519
|
+
}
|
|
520
|
+
})) || void 0;
|
|
521
|
+
return {
|
|
522
|
+
content: response.message?.content || "",
|
|
523
|
+
model: response.model || model,
|
|
524
|
+
usage,
|
|
525
|
+
finishReason: toolCalls && toolCalls.length > 0 ? "tool_calls" : mapFinishReason(response.done_reason),
|
|
526
|
+
toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : void 0
|
|
527
|
+
};
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw this.mapError(error);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async complete(prompt, options = {}) {
|
|
533
|
+
const startTime = Date.now();
|
|
534
|
+
try {
|
|
535
|
+
const model = await this.resolveModel("chat", options.model);
|
|
536
|
+
const response = await this.requestJson(
|
|
537
|
+
"/generate",
|
|
538
|
+
{
|
|
539
|
+
model,
|
|
540
|
+
prompt,
|
|
541
|
+
stream: false,
|
|
542
|
+
keep_alive: this.options.keepAlive,
|
|
543
|
+
options: this.buildRuntimeOptions(options)
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
const usage = mapUsage(response.prompt_eval_count, response.eval_count);
|
|
547
|
+
emitUsage(
|
|
548
|
+
this.options,
|
|
549
|
+
"ollama",
|
|
550
|
+
"complete",
|
|
551
|
+
response.model || model,
|
|
552
|
+
usage,
|
|
553
|
+
startTime,
|
|
554
|
+
options.usageTags
|
|
555
|
+
);
|
|
556
|
+
return {
|
|
557
|
+
content: response.response || "",
|
|
558
|
+
model: response.model || model,
|
|
559
|
+
usage,
|
|
560
|
+
finishReason: mapFinishReason(response.done_reason)
|
|
561
|
+
};
|
|
562
|
+
} catch (error) {
|
|
563
|
+
throw this.mapError(error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async message(text, options = {}) {
|
|
567
|
+
const messages = [
|
|
568
|
+
...options.history || [],
|
|
569
|
+
{ role: options.role || "user", content: text }
|
|
570
|
+
];
|
|
571
|
+
const response = await this.chat(messages, {
|
|
572
|
+
model: options.model,
|
|
573
|
+
maxTokens: options.maxTokens,
|
|
574
|
+
temperature: options.temperature,
|
|
575
|
+
topP: options.topP,
|
|
576
|
+
stop: options.stop,
|
|
577
|
+
stream: options.stream,
|
|
578
|
+
frequencyPenalty: options.frequencyPenalty,
|
|
579
|
+
presencePenalty: options.presencePenalty,
|
|
580
|
+
responseFormat: options.responseFormat,
|
|
581
|
+
seed: options.seed,
|
|
582
|
+
tools: options.tools,
|
|
583
|
+
toolChoice: options.toolChoice,
|
|
584
|
+
onProgress: options.onProgress,
|
|
585
|
+
usageTags: options.usageTags
|
|
586
|
+
});
|
|
587
|
+
return response.content;
|
|
588
|
+
}
|
|
589
|
+
async embed(text, options = {}) {
|
|
590
|
+
const startTime = Date.now();
|
|
591
|
+
try {
|
|
592
|
+
const model = await this.resolveModel("embedding", options.model);
|
|
593
|
+
const input = Array.isArray(text) ? text : [text];
|
|
594
|
+
const response = await this.requestJson("/embed", {
|
|
595
|
+
model,
|
|
596
|
+
input,
|
|
597
|
+
keep_alive: this.options.keepAlive
|
|
598
|
+
});
|
|
599
|
+
const embeddings = response.embeddings || (response.embedding ? [response.embedding] : void 0);
|
|
600
|
+
if (!embeddings || embeddings.length === 0) {
|
|
601
|
+
throw new AIError(
|
|
602
|
+
"Invalid embedding response from Ollama",
|
|
603
|
+
"INVALID_RESPONSE",
|
|
604
|
+
"ollama"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const usage = mapUsage(response.prompt_eval_count, 0);
|
|
608
|
+
emitUsage(
|
|
609
|
+
this.options,
|
|
610
|
+
"ollama",
|
|
611
|
+
"embed",
|
|
612
|
+
response.model || model,
|
|
613
|
+
usage,
|
|
614
|
+
startTime,
|
|
615
|
+
options.usageTags
|
|
616
|
+
);
|
|
617
|
+
return {
|
|
618
|
+
embeddings,
|
|
619
|
+
usage,
|
|
620
|
+
model: response.model || model
|
|
621
|
+
};
|
|
622
|
+
} catch (error) {
|
|
623
|
+
throw this.mapError(error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async embedImage(image, options = {}) {
|
|
627
|
+
const description = await this.describeImage(
|
|
628
|
+
image,
|
|
629
|
+
"Describe this image in detail for semantic embedding. Include objects, people, text, setting, colors, composition, and notable visual relationships."
|
|
630
|
+
);
|
|
631
|
+
return this.embed(description, {
|
|
632
|
+
model: options.model,
|
|
633
|
+
dimensions: options.dimensions,
|
|
634
|
+
user: options.user
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async describeImage(image, prompt, options = {}) {
|
|
638
|
+
const defaultPrompt = "Describe this image for a search index. Include objects, mood, lighting, and any visible text.";
|
|
639
|
+
const imageUrl = await this.imageToDataUrl(image);
|
|
640
|
+
const response = await this.chat(
|
|
641
|
+
[
|
|
642
|
+
{
|
|
643
|
+
role: "user",
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: prompt || defaultPrompt
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
type: "image_url",
|
|
651
|
+
image_url: {
|
|
652
|
+
url: imageUrl,
|
|
653
|
+
detail: options.detail || "auto"
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
{
|
|
660
|
+
model: options.model,
|
|
661
|
+
maxTokens: options.maxTokens || 500,
|
|
662
|
+
thinkingLevel: false
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
return response.content;
|
|
666
|
+
}
|
|
667
|
+
async generateImage(prompt, options = {}) {
|
|
668
|
+
try {
|
|
669
|
+
if (options.imageInput) {
|
|
670
|
+
throw new AIError(
|
|
671
|
+
"Ollama image generation does not support imageInput in this adapter yet.",
|
|
672
|
+
"NOT_IMPLEMENTED",
|
|
673
|
+
"ollama"
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (options.outputFormat === "url") {
|
|
677
|
+
throw new AIError(
|
|
678
|
+
"Ollama image generation only supports buffer or base64 outputs in this adapter.",
|
|
679
|
+
"NOT_SUPPORTED",
|
|
680
|
+
"ollama"
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
const model = await this.resolveModel("image_generation", options.model);
|
|
684
|
+
const response = await this.requestJson(
|
|
685
|
+
"/images/generations",
|
|
686
|
+
{
|
|
687
|
+
model,
|
|
688
|
+
prompt,
|
|
689
|
+
n: options.n || 1,
|
|
690
|
+
size: options.size || "1024x1024",
|
|
691
|
+
response_format: "b64_json",
|
|
692
|
+
quality: options.quality,
|
|
693
|
+
style: options.style
|
|
694
|
+
},
|
|
695
|
+
{ compatibility: true }
|
|
696
|
+
);
|
|
697
|
+
const images = (response.data || []).map((item) => {
|
|
698
|
+
const b64 = item.b64_json || "";
|
|
699
|
+
return {
|
|
700
|
+
data: options.outputFormat === "base64" ? b64 : Buffer.from(b64, "base64"),
|
|
701
|
+
mimeType: "image/png",
|
|
702
|
+
revisedPrompt: item.revised_prompt
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
images,
|
|
707
|
+
model
|
|
708
|
+
};
|
|
709
|
+
} catch (error) {
|
|
710
|
+
throw this.mapError(error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async *stream(messages, options = {}) {
|
|
714
|
+
const startTime = Date.now();
|
|
715
|
+
try {
|
|
716
|
+
const model = await this.resolveModel(
|
|
717
|
+
this.isVisionRequest(messages) ? "vision" : "chat",
|
|
718
|
+
options.model
|
|
719
|
+
);
|
|
720
|
+
const response = await this.requestStream("/chat", {
|
|
721
|
+
model,
|
|
722
|
+
messages: await this.mapMessagesToOllama(messages),
|
|
723
|
+
stream: true,
|
|
724
|
+
format: options.responseFormat?.type === "json_object" ? "json" : void 0,
|
|
725
|
+
tools: this.mapTools(options.tools, options.toolChoice),
|
|
726
|
+
think: this.mapThink(options),
|
|
727
|
+
keep_alive: this.options.keepAlive,
|
|
728
|
+
options: this.buildRuntimeOptions(options)
|
|
729
|
+
});
|
|
730
|
+
let finalUsage;
|
|
731
|
+
let finalModel = model;
|
|
732
|
+
for await (const chunk of this.parseNdjson(
|
|
733
|
+
response
|
|
734
|
+
)) {
|
|
735
|
+
if (chunk.model) {
|
|
736
|
+
finalModel = chunk.model;
|
|
737
|
+
}
|
|
738
|
+
if (typeof chunk.message?.content === "string" && chunk.message.content) {
|
|
739
|
+
if (options.onProgress) {
|
|
740
|
+
options.onProgress(chunk.message.content);
|
|
741
|
+
}
|
|
742
|
+
yield chunk.message.content;
|
|
743
|
+
}
|
|
744
|
+
if (chunk.done) {
|
|
745
|
+
finalUsage = mapUsage(chunk.prompt_eval_count, chunk.eval_count);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
emitUsage(
|
|
749
|
+
this.options,
|
|
750
|
+
"ollama",
|
|
751
|
+
"stream",
|
|
752
|
+
finalModel,
|
|
753
|
+
finalUsage,
|
|
754
|
+
startTime,
|
|
755
|
+
options.usageTags
|
|
756
|
+
);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
throw this.mapError(error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async countTokens(text) {
|
|
762
|
+
return Math.ceil(text.length / 4);
|
|
763
|
+
}
|
|
764
|
+
async getModels() {
|
|
765
|
+
if (this.modelListPromise) {
|
|
766
|
+
return this.modelListPromise;
|
|
767
|
+
}
|
|
768
|
+
this.modelListPromise = (async () => {
|
|
769
|
+
const response = await this.requestJson("/tags");
|
|
770
|
+
const summaries = response.models || [];
|
|
771
|
+
return Promise.all(
|
|
772
|
+
summaries.map(async (summary) => {
|
|
773
|
+
const modelId = summary.name || summary.model;
|
|
774
|
+
if (!modelId) {
|
|
775
|
+
throw new AIError(
|
|
776
|
+
"Ollama returned a model without an identifier",
|
|
777
|
+
"INVALID_RESPONSE",
|
|
778
|
+
"ollama"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
const show = await this.showModel(modelId);
|
|
782
|
+
const capabilities = parseCapabilities(modelId, show);
|
|
783
|
+
const vision = capabilities.includes("vision");
|
|
784
|
+
const functions = capabilities.includes("functions");
|
|
785
|
+
const parameterSize = show?.details?.parameter_size || summary.details?.parameter_size;
|
|
786
|
+
const family = show?.details?.family || summary.details?.family;
|
|
787
|
+
return {
|
|
788
|
+
id: modelId,
|
|
789
|
+
name: modelId,
|
|
790
|
+
description: parameterSize || family ? `${family || "Ollama"} ${parameterSize || ""}`.trim() : `Ollama model: ${modelId}`,
|
|
791
|
+
contextLength: parseContextLength(modelId, show),
|
|
792
|
+
capabilities,
|
|
793
|
+
supportsFunctions: functions,
|
|
794
|
+
supportsVision: vision
|
|
795
|
+
};
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
})().catch((error) => {
|
|
799
|
+
this.modelListPromise = void 0;
|
|
800
|
+
throw this.mapError(error);
|
|
801
|
+
});
|
|
802
|
+
return this.modelListPromise;
|
|
803
|
+
}
|
|
804
|
+
async getCapabilities() {
|
|
805
|
+
return { ...OLLAMA_CAPABILITIES };
|
|
806
|
+
}
|
|
807
|
+
async synthesizeSpeech(_text, _options) {
|
|
808
|
+
throw new AIError(
|
|
809
|
+
"TTS is not supported by the Ollama provider.",
|
|
810
|
+
"NOT_IMPLEMENTED",
|
|
811
|
+
"ollama"
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
streamSpeech(_text, _options) {
|
|
815
|
+
const error = new AIError(
|
|
816
|
+
"TTS streaming is not supported by the Ollama provider.",
|
|
817
|
+
"NOT_IMPLEMENTED",
|
|
818
|
+
"ollama"
|
|
819
|
+
);
|
|
820
|
+
return {
|
|
821
|
+
[Symbol.asyncIterator]: () => ({
|
|
822
|
+
next: () => Promise.reject(error)
|
|
823
|
+
})
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
async cloneVoice(_options) {
|
|
827
|
+
throw new AIError(
|
|
828
|
+
"Voice cloning is not supported by the Ollama provider.",
|
|
829
|
+
"NOT_IMPLEMENTED",
|
|
830
|
+
"ollama"
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
async designVoice(_options) {
|
|
834
|
+
throw new AIError(
|
|
835
|
+
"Voice design is not supported by the Ollama provider.",
|
|
836
|
+
"NOT_IMPLEMENTED",
|
|
837
|
+
"ollama"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
async getVoices(_options) {
|
|
841
|
+
throw new AIError(
|
|
842
|
+
"Voice listing is not supported by the Ollama provider.",
|
|
843
|
+
"NOT_IMPLEMENTED",
|
|
844
|
+
"ollama"
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
export {
|
|
849
|
+
OllamaProvider
|
|
850
|
+
};
|
|
851
|
+
//# sourceMappingURL=ollama-Di1ldur0.js.map
|