@nemesis-oss/ollama-sdk 1.0.0
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/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/dist/compose-DC11aDJn.d.cts +430 -0
- package/dist/compose-DC11aDJn.d.ts +430 -0
- package/dist/index.cjs +2049 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1044 -0
- package/dist/index.d.ts +1044 -0
- package/dist/index.js +1969 -0
- package/dist/index.js.map +1 -0
- package/dist/skills.cjs +185 -0
- package/dist/skills.cjs.map +1 -0
- package/dist/skills.d.cts +19 -0
- package/dist/skills.d.ts +19 -0
- package/dist/skills.js +181 -0
- package/dist/skills.js.map +1 -0
- package/package.json +137 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1969 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var DEFAULT_BASE_URL = "http://localhost:11434";
|
|
5
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6
|
+
function readEnv(name) {
|
|
7
|
+
return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
|
|
8
|
+
}
|
|
9
|
+
function resolveBaseUrl(configBaseUrl) {
|
|
10
|
+
return configBaseUrl ?? readEnv("OLLAMA_HOST") ?? DEFAULT_BASE_URL;
|
|
11
|
+
}
|
|
12
|
+
function resolveApiKey(configApiKey) {
|
|
13
|
+
return configApiKey ?? readEnv("OLLAMA_API_KEY");
|
|
14
|
+
}
|
|
15
|
+
var DEFAULT_FAILOVER_CODES = [
|
|
16
|
+
"network_error",
|
|
17
|
+
"timeout",
|
|
18
|
+
"server_error",
|
|
19
|
+
"rate_limited",
|
|
20
|
+
"auth_error",
|
|
21
|
+
// A candidate endpoint rejected the request pre-flight for a known-unsupported
|
|
22
|
+
// capability (e.g. structured output against Ollama Cloud) — try the next candidate
|
|
23
|
+
// rather than failing outright; see OllamaUnsupportedCapabilityError.
|
|
24
|
+
"unsupported_capability"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// src/errors.ts
|
|
28
|
+
var OllamaClientError = class extends Error {
|
|
29
|
+
code;
|
|
30
|
+
status;
|
|
31
|
+
retryable;
|
|
32
|
+
request;
|
|
33
|
+
response;
|
|
34
|
+
constructor(message, options) {
|
|
35
|
+
super(message, { cause: options.cause });
|
|
36
|
+
this.name = this.constructor.name;
|
|
37
|
+
this.code = options.code;
|
|
38
|
+
this.status = options.status;
|
|
39
|
+
this.retryable = options.retryable ?? false;
|
|
40
|
+
this.request = options.request;
|
|
41
|
+
this.response = options.response;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var OllamaNetworkError = class extends OllamaClientError {
|
|
45
|
+
constructor(message, options) {
|
|
46
|
+
super(message, { ...options, code: "network_error", retryable: true });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var OllamaTimeoutError = class extends OllamaClientError {
|
|
50
|
+
timeoutMs;
|
|
51
|
+
constructor(message, options) {
|
|
52
|
+
super(message, { ...options, code: "timeout", retryable: true });
|
|
53
|
+
this.timeoutMs = options?.timeoutMs;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var OllamaAuthError = class extends OllamaClientError {
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super(message, { ...options, code: "auth_error", retryable: false });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var OllamaNotFoundError = class extends OllamaClientError {
|
|
62
|
+
constructor(message, options) {
|
|
63
|
+
super(message, { ...options, code: "not_found", retryable: false });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var OllamaRateLimitError = class extends OllamaClientError {
|
|
67
|
+
retryAfterMs;
|
|
68
|
+
constructor(message, options) {
|
|
69
|
+
super(message, { ...options, code: "rate_limited", retryable: true });
|
|
70
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var OllamaServerError = class extends OllamaClientError {
|
|
74
|
+
constructor(message, options) {
|
|
75
|
+
super(message, { ...options, code: "server_error", retryable: true });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var OllamaAbortError = class extends OllamaClientError {
|
|
79
|
+
constructor(message, options) {
|
|
80
|
+
super(message, { ...options, code: "aborted", retryable: false });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var OllamaToolValidationError = class extends OllamaClientError {
|
|
84
|
+
toolName;
|
|
85
|
+
issues;
|
|
86
|
+
constructor(message, options) {
|
|
87
|
+
super(message, { ...options, code: "tool_validation_error", retryable: false });
|
|
88
|
+
this.toolName = options.toolName;
|
|
89
|
+
this.issues = options.issues;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var OllamaToolTimeoutError = class extends OllamaClientError {
|
|
93
|
+
toolName;
|
|
94
|
+
timeoutMs;
|
|
95
|
+
constructor(message, options) {
|
|
96
|
+
super(message, { ...options, code: "tool_timeout", retryable: false });
|
|
97
|
+
this.toolName = options.toolName;
|
|
98
|
+
this.timeoutMs = options.timeoutMs;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var OllamaUnsupportedCapabilityError = class extends OllamaClientError {
|
|
102
|
+
capability;
|
|
103
|
+
constructor(message, options) {
|
|
104
|
+
super(message, { ...options, code: "unsupported_capability", retryable: false });
|
|
105
|
+
this.capability = options.capability;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var OllamaAgentMaxIterationsError = class extends OllamaClientError {
|
|
109
|
+
maxIterations;
|
|
110
|
+
constructor(message, options) {
|
|
111
|
+
super(message, { ...options, code: "agent_max_iterations_exceeded", retryable: false });
|
|
112
|
+
this.maxIterations = options.maxIterations;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var OllamaMcpError = class extends OllamaClientError {
|
|
116
|
+
mcpMethod;
|
|
117
|
+
toolName;
|
|
118
|
+
constructor(message, options) {
|
|
119
|
+
super(message, { ...options, code: "mcp_error", retryable: options.retryable ?? false });
|
|
120
|
+
this.mcpMethod = options.mcpMethod;
|
|
121
|
+
this.toolName = options.toolName;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var OllamaSkillNotFoundError = class extends OllamaClientError {
|
|
125
|
+
skillName;
|
|
126
|
+
constructor(message, options) {
|
|
127
|
+
super(message, { ...options, code: "skill_not_found", retryable: false });
|
|
128
|
+
this.skillName = options.skillName;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var OllamaSkillInvalidError = class extends OllamaClientError {
|
|
132
|
+
skillName;
|
|
133
|
+
path;
|
|
134
|
+
constructor(message, options) {
|
|
135
|
+
super(message, { ...options, code: "skill_invalid", retryable: false });
|
|
136
|
+
this.skillName = options.skillName;
|
|
137
|
+
this.path = options.path;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var OllamaGenericClientError = class extends OllamaClientError {
|
|
141
|
+
constructor(message, options) {
|
|
142
|
+
super(message, { ...options, code: "client_error" });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function isDomAbortError(error) {
|
|
146
|
+
return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError";
|
|
147
|
+
}
|
|
148
|
+
function statusToError(status, message, options) {
|
|
149
|
+
if (status === 401 || status === 403) {
|
|
150
|
+
return new OllamaAuthError(message, { ...options, status });
|
|
151
|
+
}
|
|
152
|
+
if (status === 404) {
|
|
153
|
+
return new OllamaNotFoundError(message, { ...options, status });
|
|
154
|
+
}
|
|
155
|
+
if (status === 429) {
|
|
156
|
+
return new OllamaRateLimitError(message, { ...options, status });
|
|
157
|
+
}
|
|
158
|
+
if (status >= 500) {
|
|
159
|
+
return new OllamaServerError(message, { ...options, status });
|
|
160
|
+
}
|
|
161
|
+
return new OllamaGenericClientError(message, { ...options, status });
|
|
162
|
+
}
|
|
163
|
+
function mapError(error, context = {}) {
|
|
164
|
+
if (error instanceof OllamaClientError) {
|
|
165
|
+
return error;
|
|
166
|
+
}
|
|
167
|
+
if (isDomAbortError(error)) {
|
|
168
|
+
return new OllamaAbortError(error.message || "Request was aborted", {
|
|
169
|
+
cause: error,
|
|
170
|
+
request: context.request
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (error instanceof TypeError) {
|
|
174
|
+
return new OllamaNetworkError(error.message || "Network request failed", {
|
|
175
|
+
cause: error,
|
|
176
|
+
request: context.request
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (context.response?.status !== void 0) {
|
|
180
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
181
|
+
return statusToError(context.response.status, message2, {
|
|
182
|
+
cause: error,
|
|
183
|
+
request: context.request,
|
|
184
|
+
response: context.response
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
return new OllamaGenericClientError(message, {
|
|
189
|
+
cause: error,
|
|
190
|
+
request: context.request,
|
|
191
|
+
response: context.response
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/logger.ts
|
|
196
|
+
var NOOP_LOGGER = {
|
|
197
|
+
debug: () => void 0,
|
|
198
|
+
info: () => void 0,
|
|
199
|
+
warn: () => void 0,
|
|
200
|
+
error: () => void 0
|
|
201
|
+
};
|
|
202
|
+
function createConsoleLogger(prefix = "[@nemesis-oss/ollama-sdk]") {
|
|
203
|
+
return {
|
|
204
|
+
debug: (msg, ctx) => console.debug(`${prefix} ${msg}`, ctx ?? ""),
|
|
205
|
+
info: (msg, ctx) => console.info(`${prefix} ${msg}`, ctx ?? ""),
|
|
206
|
+
warn: (msg, ctx) => console.warn(`${prefix} ${msg}`, ctx ?? ""),
|
|
207
|
+
error: (msg, ctx) => console.error(`${prefix} ${msg}`, ctx ?? "")
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/providers/endpoint-registry.ts
|
|
212
|
+
var EndpointRegistry = class {
|
|
213
|
+
endpoints;
|
|
214
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
215
|
+
lastFailures = /* @__PURE__ */ new Map();
|
|
216
|
+
failureThreshold;
|
|
217
|
+
cooldownMs;
|
|
218
|
+
now;
|
|
219
|
+
constructor(endpoints, options = {}) {
|
|
220
|
+
this.endpoints = endpoints.length > 0 ? endpoints : [{ name: "default", baseUrl: "http://localhost:11434" }];
|
|
221
|
+
this.failureThreshold = options.failureThreshold ?? 3;
|
|
222
|
+
this.cooldownMs = options.cooldownMs ?? 3e4;
|
|
223
|
+
this.now = options.now ?? Date.now;
|
|
224
|
+
}
|
|
225
|
+
candidates() {
|
|
226
|
+
const currentTime = this.now();
|
|
227
|
+
const healthy = [];
|
|
228
|
+
const coolingDown = [];
|
|
229
|
+
for (const ep of this.endpoints) {
|
|
230
|
+
const failures = this.failureCounts.get(ep.name) ?? 0;
|
|
231
|
+
const lastFail = this.lastFailures.get(ep.name) ?? 0;
|
|
232
|
+
const isCooling = failures >= this.failureThreshold && currentTime - lastFail < this.cooldownMs;
|
|
233
|
+
if (!isCooling) {
|
|
234
|
+
healthy.push(ep);
|
|
235
|
+
} else {
|
|
236
|
+
coolingDown.push({ endpoint: ep, recoveredAt: lastFail + this.cooldownMs });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (healthy.length > 0) {
|
|
240
|
+
return healthy.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
241
|
+
}
|
|
242
|
+
return coolingDown.sort((a, b) => a.recoveredAt - b.recoveredAt).map((c) => c.endpoint);
|
|
243
|
+
}
|
|
244
|
+
reportSuccess(name) {
|
|
245
|
+
this.failureCounts.delete(name);
|
|
246
|
+
this.lastFailures.delete(name);
|
|
247
|
+
}
|
|
248
|
+
reportFailure(name) {
|
|
249
|
+
const current = (this.failureCounts.get(name) ?? 0) + 1;
|
|
250
|
+
this.failureCounts.set(name, current);
|
|
251
|
+
this.lastFailures.set(name, this.now());
|
|
252
|
+
}
|
|
253
|
+
list() {
|
|
254
|
+
return this.endpoints;
|
|
255
|
+
}
|
|
256
|
+
status() {
|
|
257
|
+
const currentTime = this.now();
|
|
258
|
+
return this.endpoints.map((ep) => {
|
|
259
|
+
const failures = this.failureCounts.get(ep.name) ?? 0;
|
|
260
|
+
const lastFail = this.lastFailures.get(ep.name);
|
|
261
|
+
const isCoolingDown = failures >= this.failureThreshold && lastFail !== void 0 && currentTime - lastFail < this.cooldownMs;
|
|
262
|
+
return {
|
|
263
|
+
endpoint: ep,
|
|
264
|
+
failureCount: failures,
|
|
265
|
+
...lastFail !== void 0 ? { lastFailureTimestamp: lastFail } : {},
|
|
266
|
+
isCoolingDown
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/streaming/ndjson.ts
|
|
273
|
+
async function* parseNdjsonStream(stream) {
|
|
274
|
+
const reader = stream.getReader();
|
|
275
|
+
const decoder = new TextDecoder();
|
|
276
|
+
let buffer = "";
|
|
277
|
+
try {
|
|
278
|
+
while (true) {
|
|
279
|
+
const { done, value } = await reader.read();
|
|
280
|
+
if (done) break;
|
|
281
|
+
buffer += decoder.decode(value, { stream: true });
|
|
282
|
+
const lines = buffer.split("\n");
|
|
283
|
+
buffer = lines.pop() ?? "";
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
const trimmed = line.trim();
|
|
286
|
+
if (!trimmed) continue;
|
|
287
|
+
try {
|
|
288
|
+
yield JSON.parse(trimmed);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
throw new OllamaGenericClientError(`Failed to parse stream JSON chunk: ${trimmed}`, {
|
|
291
|
+
cause: err
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (buffer.trim()) {
|
|
297
|
+
yield JSON.parse(buffer.trim());
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
reader.releaseLock();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/telemetry/tracer.ts
|
|
305
|
+
var INSTRUMENTATION_NAME = "@nemesis-oss/ollama-sdk";
|
|
306
|
+
var INSTRUMENTATION_VERSION = "1.0.0";
|
|
307
|
+
var apiPromise;
|
|
308
|
+
function loadApi() {
|
|
309
|
+
apiPromise ??= import('@opentelemetry/api').catch(() => void 0);
|
|
310
|
+
return apiPromise;
|
|
311
|
+
}
|
|
312
|
+
function cleanAttributes(attributes) {
|
|
313
|
+
const clean = {};
|
|
314
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
315
|
+
if (value !== void 0) {
|
|
316
|
+
clean[key] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return clean;
|
|
320
|
+
}
|
|
321
|
+
async function withSpan(name, attributes, fn) {
|
|
322
|
+
const api = await loadApi();
|
|
323
|
+
if (!api) {
|
|
324
|
+
return fn(void 0);
|
|
325
|
+
}
|
|
326
|
+
const tracer = api.trace.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
|
|
327
|
+
return tracer.startActiveSpan(name, async (span) => {
|
|
328
|
+
span.setAttributes(cleanAttributes(attributes));
|
|
329
|
+
try {
|
|
330
|
+
return await fn(span);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
333
|
+
span.recordException(error);
|
|
334
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
|
|
335
|
+
throw error;
|
|
336
|
+
} finally {
|
|
337
|
+
span.end();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function addSpanEvent(span, name, attributes) {
|
|
342
|
+
span?.addEvent(name, attributes ? cleanAttributes(attributes) : void 0);
|
|
343
|
+
}
|
|
344
|
+
async function setSpanError(span, error) {
|
|
345
|
+
if (!span) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const api = await loadApi();
|
|
349
|
+
span.recordException(error);
|
|
350
|
+
span.setStatus({ code: api?.SpanStatusCode.ERROR ?? 2, message: error.message });
|
|
351
|
+
}
|
|
352
|
+
var ATTR_HTTP_REQUEST_METHOD = "http.request.method";
|
|
353
|
+
var ATTR_URL_FULL = "url.full";
|
|
354
|
+
var ATTR_SERVER_ADDRESS = "server.address";
|
|
355
|
+
var ATTR_SERVER_PORT = "server.port";
|
|
356
|
+
var ATTR_HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
|
|
357
|
+
var ATTR_GEN_AI_SYSTEM = "gen_ai.system";
|
|
358
|
+
var ATTR_GEN_AI_OPERATION_NAME = "gen_ai.operation.name";
|
|
359
|
+
var ATTR_GEN_AI_REQUEST_MODEL = "gen_ai.request.model";
|
|
360
|
+
var ATTR_GEN_AI_RESPONSE_MODEL = "gen_ai.response.model";
|
|
361
|
+
var ATTR_GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens";
|
|
362
|
+
var ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens";
|
|
363
|
+
var ATTR_GEN_AI_TOOL_NAME = "gen_ai.tool.name";
|
|
364
|
+
var ATTR_GEN_AI_TOOL_CALL_ID = "gen_ai.tool.call.id";
|
|
365
|
+
var ATTR_OLLAMA_ENDPOINT_NAME = "ollama.endpoint.name";
|
|
366
|
+
var ATTR_OLLAMA_ENDPOINT_ATTEMPT = "ollama.endpoint.attempt";
|
|
367
|
+
var ATTR_OLLAMA_AGENT_MAX_ITERATIONS = "ollama.agent.max_iterations";
|
|
368
|
+
var ATTR_OLLAMA_AGENT_ITERATION = "ollama.agent.iteration";
|
|
369
|
+
var GEN_AI_SYSTEM_OLLAMA = "ollama";
|
|
370
|
+
|
|
371
|
+
// src/transport/http.ts
|
|
372
|
+
function routeTemplate(path) {
|
|
373
|
+
return path.replace(/^\/api\/blobs\/.+$/, "/api/blobs/{digest}");
|
|
374
|
+
}
|
|
375
|
+
function httpSpanAttributes(method, url) {
|
|
376
|
+
const target = new URL(url);
|
|
377
|
+
return {
|
|
378
|
+
[ATTR_HTTP_REQUEST_METHOD]: method,
|
|
379
|
+
[ATTR_URL_FULL]: url,
|
|
380
|
+
[ATTR_SERVER_ADDRESS]: target.hostname,
|
|
381
|
+
[ATTR_SERVER_PORT]: target.port ? Number(target.port) : void 0
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
var HttpClient = class {
|
|
385
|
+
baseUrl;
|
|
386
|
+
apiKey;
|
|
387
|
+
defaultHeaders;
|
|
388
|
+
fetchImpl;
|
|
389
|
+
constructor(options) {
|
|
390
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
391
|
+
this.apiKey = options.apiKey;
|
|
392
|
+
this.defaultHeaders = options.headers ?? {};
|
|
393
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
394
|
+
}
|
|
395
|
+
buildHeaders(customHeaders) {
|
|
396
|
+
const headers = {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
...this.defaultHeaders,
|
|
399
|
+
...customHeaders
|
|
400
|
+
};
|
|
401
|
+
if (this.apiKey) {
|
|
402
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
403
|
+
}
|
|
404
|
+
return headers;
|
|
405
|
+
}
|
|
406
|
+
async request(options) {
|
|
407
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
408
|
+
const method = options.method ?? (options.body !== void 0 || options.rawBody !== void 0 ? "POST" : "GET");
|
|
409
|
+
const headers = this.buildHeaders(options.headers);
|
|
410
|
+
if (options.rawBody !== void 0 && !options.headers?.["Content-Type"]) {
|
|
411
|
+
delete headers["Content-Type"];
|
|
412
|
+
}
|
|
413
|
+
return withSpan(
|
|
414
|
+
`${method} ${routeTemplate(options.path)}`,
|
|
415
|
+
httpSpanAttributes(method, url),
|
|
416
|
+
async (span) => {
|
|
417
|
+
try {
|
|
418
|
+
const bodyInit = options.rawBody !== void 0 ? options.rawBody : options.body !== void 0 ? JSON.stringify(options.body) : void 0;
|
|
419
|
+
const init = {
|
|
420
|
+
method,
|
|
421
|
+
headers,
|
|
422
|
+
...bodyInit !== void 0 ? { body: bodyInit } : {},
|
|
423
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
424
|
+
};
|
|
425
|
+
const response = await this.fetchImpl(url, init);
|
|
426
|
+
span?.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
let errorBody;
|
|
429
|
+
try {
|
|
430
|
+
errorBody = await response.json();
|
|
431
|
+
} catch {
|
|
432
|
+
errorBody = await response.text();
|
|
433
|
+
}
|
|
434
|
+
const message = typeof errorBody === "object" && errorBody !== null && "error" in errorBody ? String(errorBody.error) : `HTTP ${response.status} ${response.statusText}`;
|
|
435
|
+
throw mapError(new Error(message), {
|
|
436
|
+
request: { method, url },
|
|
437
|
+
response: { status: response.status, body: errorBody }
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (options.method === "HEAD" || response.status === 204) {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
if (typeof response.text === "function") {
|
|
444
|
+
const text = await response.text();
|
|
445
|
+
return text ? JSON.parse(text) : void 0;
|
|
446
|
+
}
|
|
447
|
+
if (typeof response.json === "function") {
|
|
448
|
+
return await response.json();
|
|
449
|
+
}
|
|
450
|
+
return void 0;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
throw mapError(err, { request: { method, url } });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
async requestStream(options) {
|
|
458
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
459
|
+
const method = options.method ?? "POST";
|
|
460
|
+
const headers = this.buildHeaders(options.headers);
|
|
461
|
+
return withSpan(
|
|
462
|
+
`${method} ${routeTemplate(options.path)}`,
|
|
463
|
+
httpSpanAttributes(method, url),
|
|
464
|
+
async (span) => {
|
|
465
|
+
try {
|
|
466
|
+
const init = {
|
|
467
|
+
method,
|
|
468
|
+
headers,
|
|
469
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
|
|
470
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
471
|
+
};
|
|
472
|
+
const response = await this.fetchImpl(url, init);
|
|
473
|
+
span?.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
474
|
+
if (!response.ok) {
|
|
475
|
+
const errorText = await response.text();
|
|
476
|
+
throw mapError(new Error(errorText || `HTTP ${response.status}`), {
|
|
477
|
+
request: { method, url },
|
|
478
|
+
response: { status: response.status, body: errorText }
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (!response.body) {
|
|
482
|
+
throw mapError(new Error("Response body is null, cannot stream"), {
|
|
483
|
+
request: { method, url }
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const stream = parseNdjsonStream(response.body);
|
|
487
|
+
const abortable = {
|
|
488
|
+
[Symbol.asyncIterator]() {
|
|
489
|
+
return stream[Symbol.asyncIterator]();
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
return abortable;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
throw mapError(err, { request: { method, url } });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/providers/health-check.ts
|
|
502
|
+
async function checkEndpointHealth(endpoint, fetchImpl, timeoutMs = 5e3) {
|
|
503
|
+
const client = new HttpClient({
|
|
504
|
+
baseUrl: endpoint.baseUrl,
|
|
505
|
+
apiKey: endpoint.apiKey,
|
|
506
|
+
headers: endpoint.headers,
|
|
507
|
+
fetch: fetchImpl
|
|
508
|
+
});
|
|
509
|
+
const startTime = Date.now();
|
|
510
|
+
const controller = new AbortController();
|
|
511
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
512
|
+
try {
|
|
513
|
+
const res = await client.request({
|
|
514
|
+
path: "/api/version",
|
|
515
|
+
method: "GET",
|
|
516
|
+
signal: controller.signal
|
|
517
|
+
});
|
|
518
|
+
const latencyMs = Date.now() - startTime;
|
|
519
|
+
return {
|
|
520
|
+
name: endpoint.name,
|
|
521
|
+
baseUrl: endpoint.baseUrl,
|
|
522
|
+
reachable: true,
|
|
523
|
+
latencyMs,
|
|
524
|
+
version: res.version
|
|
525
|
+
};
|
|
526
|
+
} catch (err) {
|
|
527
|
+
const latencyMs = Date.now() - startTime;
|
|
528
|
+
return {
|
|
529
|
+
name: endpoint.name,
|
|
530
|
+
baseUrl: endpoint.baseUrl,
|
|
531
|
+
reachable: false,
|
|
532
|
+
latencyMs,
|
|
533
|
+
error: err instanceof Error ? err.message : String(err)
|
|
534
|
+
};
|
|
535
|
+
} finally {
|
|
536
|
+
clearTimeout(timer);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/capabilities/capabilities.ts
|
|
541
|
+
function inferRuntimeMode(baseUrl) {
|
|
542
|
+
try {
|
|
543
|
+
const url = new URL(baseUrl);
|
|
544
|
+
const host = url.hostname.toLowerCase();
|
|
545
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.")) {
|
|
546
|
+
return "local";
|
|
547
|
+
}
|
|
548
|
+
return "cloud";
|
|
549
|
+
} catch {
|
|
550
|
+
return "unknown";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function detectModelCapabilities(http, model) {
|
|
554
|
+
const showRes = await http.request({
|
|
555
|
+
path: "/api/show",
|
|
556
|
+
body: { model }
|
|
557
|
+
});
|
|
558
|
+
const reported = showRes.capabilities ?? [];
|
|
559
|
+
const reportedSet = new Set(reported.map((c) => c.toLowerCase()));
|
|
560
|
+
return {
|
|
561
|
+
model,
|
|
562
|
+
reported,
|
|
563
|
+
supportsTools: reportedSet.has("tools"),
|
|
564
|
+
supportsVision: reportedSet.has("vision"),
|
|
565
|
+
supportsEmbedding: reportedSet.has("embedding"),
|
|
566
|
+
supportsCompletion: reportedSet.has("completion") || !reportedSet.has("embedding"),
|
|
567
|
+
supportsThinking: reportedSet.has("thinking"),
|
|
568
|
+
supportsStreaming: true,
|
|
569
|
+
supportsStructuredOutputRequest: inferRuntimeMode(http.baseUrl) !== "cloud"
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
async function listAvailableModels(http) {
|
|
573
|
+
const res = await http.request({
|
|
574
|
+
path: "/api/tags",
|
|
575
|
+
method: "GET"
|
|
576
|
+
});
|
|
577
|
+
return [...res.models];
|
|
578
|
+
}
|
|
579
|
+
function extractJsonSubstring(text) {
|
|
580
|
+
const trimmed = text.trim();
|
|
581
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
582
|
+
return trimmed;
|
|
583
|
+
}
|
|
584
|
+
const match = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
585
|
+
if (match && match[1]) {
|
|
586
|
+
return match[1].trim();
|
|
587
|
+
}
|
|
588
|
+
const firstBrace = trimmed.indexOf("{");
|
|
589
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
590
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
591
|
+
return trimmed.slice(firstBrace, lastBrace + 1);
|
|
592
|
+
}
|
|
593
|
+
return trimmed;
|
|
594
|
+
}
|
|
595
|
+
function zodV3ToJsonSchema(schema) {
|
|
596
|
+
const def = schema._def;
|
|
597
|
+
if (!def) {
|
|
598
|
+
return { type: "object" };
|
|
599
|
+
}
|
|
600
|
+
switch (def["typeName"]) {
|
|
601
|
+
case "ZodObject": {
|
|
602
|
+
const shape = def["shape"]();
|
|
603
|
+
const properties = {};
|
|
604
|
+
const required = [];
|
|
605
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
606
|
+
properties[key] = zodV3ToJsonSchema(value);
|
|
607
|
+
if (!isZodV3Optional(value)) {
|
|
608
|
+
required.push(key);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties,
|
|
614
|
+
...required.length > 0 ? { required } : {}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
case "ZodString":
|
|
618
|
+
return { type: "string" };
|
|
619
|
+
case "ZodNumber":
|
|
620
|
+
return { type: "number" };
|
|
621
|
+
case "ZodBoolean":
|
|
622
|
+
return { type: "boolean" };
|
|
623
|
+
case "ZodDate":
|
|
624
|
+
return { type: "string", format: "date-time" };
|
|
625
|
+
case "ZodArray":
|
|
626
|
+
return { type: "array", items: zodV3ToJsonSchema(def["type"]) };
|
|
627
|
+
case "ZodEnum":
|
|
628
|
+
return { type: "string", enum: def["values"] };
|
|
629
|
+
case "ZodNativeEnum":
|
|
630
|
+
return { enum: Object.values(def["values"]) };
|
|
631
|
+
case "ZodLiteral":
|
|
632
|
+
return { const: def["value"] };
|
|
633
|
+
case "ZodOptional":
|
|
634
|
+
case "ZodNullable":
|
|
635
|
+
case "ZodDefault":
|
|
636
|
+
return zodV3ToJsonSchema(def["innerType"]);
|
|
637
|
+
case "ZodEffects":
|
|
638
|
+
return zodV3ToJsonSchema(def["schema"]);
|
|
639
|
+
case "ZodUnion":
|
|
640
|
+
return { anyOf: def["options"].map((option) => zodV3ToJsonSchema(option)) };
|
|
641
|
+
case "ZodRecord":
|
|
642
|
+
return { type: "object", additionalProperties: zodV3ToJsonSchema(def["valueType"]) };
|
|
643
|
+
default:
|
|
644
|
+
return { type: "object" };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function isZodV3Optional(schema) {
|
|
648
|
+
const typeName = schema._def?.typeName;
|
|
649
|
+
return typeName === "ZodOptional" || typeName === "ZodDefault";
|
|
650
|
+
}
|
|
651
|
+
function zodToJsonSchema(schema) {
|
|
652
|
+
if (typeof z.toJSONSchema === "function") {
|
|
653
|
+
return z.toJSONSchema(schema);
|
|
654
|
+
}
|
|
655
|
+
return zodV3ToJsonSchema(schema);
|
|
656
|
+
}
|
|
657
|
+
function parseStructuredOutput(rawJson, schema, toolName = "structured_output") {
|
|
658
|
+
let parsed;
|
|
659
|
+
const jsonStr = extractJsonSubstring(rawJson);
|
|
660
|
+
try {
|
|
661
|
+
parsed = JSON.parse(jsonStr);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
throw new OllamaToolValidationError(`Failed to parse structured output as JSON: ${rawJson}`, {
|
|
664
|
+
toolName,
|
|
665
|
+
cause: err
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const result = schema.safeParse(parsed);
|
|
669
|
+
if (!result.success) {
|
|
670
|
+
throw new OllamaToolValidationError(`Structured output failed schema validation`, {
|
|
671
|
+
toolName,
|
|
672
|
+
issues: result.error.issues,
|
|
673
|
+
cause: result.error
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return result.data;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/usage.ts
|
|
680
|
+
var NANOS_PER_MS = 1e6;
|
|
681
|
+
var NANOS_PER_SECOND = 1e9;
|
|
682
|
+
function extractUsage(raw) {
|
|
683
|
+
const promptTokens = raw.prompt_eval_count ?? 0;
|
|
684
|
+
const completionTokens = raw.eval_count ?? 0;
|
|
685
|
+
const totalTokens = promptTokens + completionTokens;
|
|
686
|
+
let tokensPerSecond;
|
|
687
|
+
if (raw.eval_count && raw.eval_duration && raw.eval_duration > 0) {
|
|
688
|
+
tokensPerSecond = Number((raw.eval_count / raw.eval_duration * NANOS_PER_SECOND).toFixed(2));
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
promptTokens,
|
|
692
|
+
completionTokens,
|
|
693
|
+
totalTokens,
|
|
694
|
+
...tokensPerSecond !== void 0 ? { tokensPerSecond } : {},
|
|
695
|
+
...raw.load_duration !== void 0 ? { loadDurationMs: raw.load_duration / NANOS_PER_MS } : {},
|
|
696
|
+
...raw.prompt_eval_duration !== void 0 ? { promptEvalDurationMs: raw.prompt_eval_duration / NANOS_PER_MS } : {},
|
|
697
|
+
...raw.eval_duration !== void 0 ? { evalDurationMs: raw.eval_duration / NANOS_PER_MS } : {},
|
|
698
|
+
...raw.total_duration !== void 0 ? { totalDurationMs: raw.total_duration / NANOS_PER_MS } : {}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/tools/tool-call-id.ts
|
|
703
|
+
function generateToolCallId() {
|
|
704
|
+
return `call_${crypto.randomUUID()}`;
|
|
705
|
+
}
|
|
706
|
+
function ensureToolCallIds(toolCalls) {
|
|
707
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
708
|
+
return toolCalls;
|
|
709
|
+
}
|
|
710
|
+
return toolCalls.map((tc) => tc.id !== void 0 ? tc : { ...tc, id: generateToolCallId() });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/streaming/stream.ts
|
|
714
|
+
var OllamaStream = class {
|
|
715
|
+
constructor(source, mapChunk, aggregate, initial) {
|
|
716
|
+
this.source = source;
|
|
717
|
+
this.mapChunk = mapChunk;
|
|
718
|
+
this.aggregate = aggregate;
|
|
719
|
+
this.initial = initial;
|
|
720
|
+
this.finalResultPromise = new Promise((resolve, reject) => {
|
|
721
|
+
this.resolveFinal = resolve;
|
|
722
|
+
this.rejectFinal = reject;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
source;
|
|
726
|
+
mapChunk;
|
|
727
|
+
aggregate;
|
|
728
|
+
initial;
|
|
729
|
+
mode = "unconsumed";
|
|
730
|
+
listeners = /* @__PURE__ */ new Map();
|
|
731
|
+
finalResultPromise;
|
|
732
|
+
resolveFinal;
|
|
733
|
+
rejectFinal;
|
|
734
|
+
get finalResult() {
|
|
735
|
+
return this.finalResultPromise;
|
|
736
|
+
}
|
|
737
|
+
abort() {
|
|
738
|
+
this.source.abort?.();
|
|
739
|
+
}
|
|
740
|
+
on(type, listener) {
|
|
741
|
+
if (this.mode === "iterator") {
|
|
742
|
+
throw new Error(
|
|
743
|
+
"Cannot register event listeners: this stream is already being consumed via async iteration."
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const genericListener = listener;
|
|
747
|
+
let set = this.listeners.get(type);
|
|
748
|
+
if (!set) {
|
|
749
|
+
set = /* @__PURE__ */ new Set();
|
|
750
|
+
this.listeners.set(type, set);
|
|
751
|
+
}
|
|
752
|
+
set.add(genericListener);
|
|
753
|
+
if (this.mode !== "events") {
|
|
754
|
+
this.mode = "events";
|
|
755
|
+
void this.pump();
|
|
756
|
+
}
|
|
757
|
+
return () => {
|
|
758
|
+
set?.delete(genericListener);
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
emit(event) {
|
|
762
|
+
for (const listener of this.listeners.get(event.type) ?? []) {
|
|
763
|
+
listener(event);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async pump() {
|
|
767
|
+
let accumulated = this.initial;
|
|
768
|
+
try {
|
|
769
|
+
for await (const chunk of this.source) {
|
|
770
|
+
accumulated = this.aggregate(accumulated, chunk);
|
|
771
|
+
for (const event of this.mapChunk(chunk, accumulated)) {
|
|
772
|
+
this.emit(event);
|
|
773
|
+
if (event.type === "done") {
|
|
774
|
+
this.resolveFinal(event.data.result);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const mapped = mapError(error);
|
|
780
|
+
this.emit({ type: "error", data: { error: mapped } });
|
|
781
|
+
this.rejectFinal(mapped);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async *[Symbol.asyncIterator]() {
|
|
785
|
+
if (this.mode === "events") {
|
|
786
|
+
throw new Error(
|
|
787
|
+
"Cannot use async iteration: this stream already has event listeners registered via .on()."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
this.mode = "iterator";
|
|
791
|
+
let accumulated = this.initial;
|
|
792
|
+
try {
|
|
793
|
+
for await (const chunk of this.source) {
|
|
794
|
+
accumulated = this.aggregate(accumulated, chunk);
|
|
795
|
+
const events = this.mapChunk(chunk, accumulated);
|
|
796
|
+
for (const event of events) {
|
|
797
|
+
if (event.type === "done") {
|
|
798
|
+
this.resolveFinal(event.data.result);
|
|
799
|
+
}
|
|
800
|
+
yield event;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const mapped = mapError(error);
|
|
805
|
+
this.rejectFinal(mapped);
|
|
806
|
+
yield { type: "error", data: { error: mapped } };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/streaming/normalize.ts
|
|
812
|
+
function aggregateChat(accumulated, chunk) {
|
|
813
|
+
const message = chunk.message ?? { role: "assistant", content: "" };
|
|
814
|
+
return {
|
|
815
|
+
message: {
|
|
816
|
+
role: message.role || accumulated.message.role,
|
|
817
|
+
content: accumulated.message.content + (message.content ?? ""),
|
|
818
|
+
thinking: message.thinking !== void 0 ? (accumulated.message.thinking ?? "") + message.thinking : accumulated.message.thinking,
|
|
819
|
+
tool_calls: message.tool_calls?.length ? [...accumulated.message.tool_calls ?? [], ...message.tool_calls] : accumulated.message.tool_calls
|
|
820
|
+
},
|
|
821
|
+
model: chunk.model,
|
|
822
|
+
done: chunk.done,
|
|
823
|
+
totalDurationMs: chunk.done && chunk.total_duration !== void 0 ? chunk.total_duration / NANOS_PER_MS : accumulated.totalDurationMs,
|
|
824
|
+
usage: chunk.done ? extractUsage(chunk) : accumulated.usage,
|
|
825
|
+
raw: chunk
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function mapChatChunk(chunk, aggregated) {
|
|
829
|
+
const events = [];
|
|
830
|
+
const message = chunk.message;
|
|
831
|
+
if (message?.thinking) {
|
|
832
|
+
events.push({ type: "thinking", data: { delta: message.thinking } });
|
|
833
|
+
}
|
|
834
|
+
for (const toolCall of message?.tool_calls ?? []) {
|
|
835
|
+
events.push({ type: "tool_call", data: { toolCall } });
|
|
836
|
+
}
|
|
837
|
+
if (message?.content) {
|
|
838
|
+
events.push({ type: "token", data: { delta: message.content, role: message.role } });
|
|
839
|
+
}
|
|
840
|
+
events.push({ type: "message", data: { chunk } });
|
|
841
|
+
if (chunk.done) {
|
|
842
|
+
events.push({ type: "done", data: { result: aggregated } });
|
|
843
|
+
}
|
|
844
|
+
return events;
|
|
845
|
+
}
|
|
846
|
+
function withStableToolCallIds(source) {
|
|
847
|
+
async function* generator() {
|
|
848
|
+
for await (const chunk of source) {
|
|
849
|
+
const toolCalls = chunk.message?.tool_calls;
|
|
850
|
+
if (!toolCalls?.length) {
|
|
851
|
+
yield chunk;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
yield { ...chunk, message: { ...chunk.message, tool_calls: ensureToolCallIds(toolCalls) } };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return Object.assign(generator(), { abort: source.abort });
|
|
858
|
+
}
|
|
859
|
+
function normalizeChatStream(source) {
|
|
860
|
+
const initial = {
|
|
861
|
+
message: { role: "assistant", content: "" },
|
|
862
|
+
model: "",
|
|
863
|
+
done: false
|
|
864
|
+
};
|
|
865
|
+
return new OllamaStream(withStableToolCallIds(source), mapChatChunk, aggregateChat, initial);
|
|
866
|
+
}
|
|
867
|
+
function aggregateGenerate(accumulated, chunk) {
|
|
868
|
+
return {
|
|
869
|
+
response: accumulated.response + (chunk.response ?? ""),
|
|
870
|
+
model: chunk.model,
|
|
871
|
+
done: chunk.done,
|
|
872
|
+
totalDurationMs: chunk.done && chunk.total_duration !== void 0 ? chunk.total_duration / NANOS_PER_MS : accumulated.totalDurationMs,
|
|
873
|
+
usage: chunk.done ? extractUsage(chunk) : accumulated.usage,
|
|
874
|
+
raw: chunk
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function mapGenerateChunk(chunk, aggregated) {
|
|
878
|
+
const events = [];
|
|
879
|
+
if (chunk.thinking) {
|
|
880
|
+
events.push({ type: "thinking", data: { delta: chunk.thinking } });
|
|
881
|
+
}
|
|
882
|
+
if (chunk.response) {
|
|
883
|
+
events.push({ type: "token", data: { delta: chunk.response, role: "assistant" } });
|
|
884
|
+
}
|
|
885
|
+
events.push({ type: "message", data: { chunk } });
|
|
886
|
+
if (chunk.done) {
|
|
887
|
+
events.push({ type: "done", data: { result: aggregated } });
|
|
888
|
+
}
|
|
889
|
+
return events;
|
|
890
|
+
}
|
|
891
|
+
function normalizeGenerateStream(source) {
|
|
892
|
+
const initial = { response: "", model: "", done: false };
|
|
893
|
+
return new OllamaStream(source, mapGenerateChunk, aggregateGenerate, initial);
|
|
894
|
+
}
|
|
895
|
+
function aggregateProgress(_accumulated, chunk) {
|
|
896
|
+
return {
|
|
897
|
+
status: chunk.status,
|
|
898
|
+
completed: chunk.completed,
|
|
899
|
+
total: chunk.total,
|
|
900
|
+
done: chunk.status === "success",
|
|
901
|
+
raw: chunk
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function mapProgressChunk(chunk, aggregated) {
|
|
905
|
+
const events = [
|
|
906
|
+
{ type: "message", data: { chunk } }
|
|
907
|
+
];
|
|
908
|
+
if (chunk.status === "success") {
|
|
909
|
+
events.push({ type: "done", data: { result: aggregated } });
|
|
910
|
+
}
|
|
911
|
+
return events;
|
|
912
|
+
}
|
|
913
|
+
function normalizeProgressStream(source) {
|
|
914
|
+
const initial = { status: "", done: false };
|
|
915
|
+
return new OllamaStream(source, mapProgressChunk, aggregateProgress, initial);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/transport/backoff.ts
|
|
919
|
+
var DEFAULT_BACKOFF = {
|
|
920
|
+
initialDelayMs: 500,
|
|
921
|
+
maxDelayMs: 3e4,
|
|
922
|
+
backoffFactor: 2
|
|
923
|
+
};
|
|
924
|
+
function calculateBackoff(attempt, options = DEFAULT_BACKOFF) {
|
|
925
|
+
const exponential = options.initialDelayMs * Math.pow(options.backoffFactor, attempt);
|
|
926
|
+
const ceiling = Math.min(exponential, options.maxDelayMs);
|
|
927
|
+
return Math.floor(Math.random() * (ceiling + 1));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/transport/retry.ts
|
|
931
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
932
|
+
maxRetries: 3,
|
|
933
|
+
backoff: DEFAULT_BACKOFF
|
|
934
|
+
};
|
|
935
|
+
function isRetryableDefault(error) {
|
|
936
|
+
if (error instanceof OllamaClientError) {
|
|
937
|
+
return error.retryable;
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
942
|
+
async function withRetry(operation, config = DEFAULT_RETRY_CONFIG) {
|
|
943
|
+
let attempt = 0;
|
|
944
|
+
while (true) {
|
|
945
|
+
try {
|
|
946
|
+
return await operation(attempt);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
949
|
+
const shouldRetry = config.shouldRetry ?? isRetryableDefault;
|
|
950
|
+
if (attempt >= config.maxRetries || !shouldRetry(error, attempt)) {
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
const delayMs = calculateBackoff(attempt, config.backoff);
|
|
954
|
+
config.onRetry?.(error, attempt, delayMs);
|
|
955
|
+
await sleep(delayMs);
|
|
956
|
+
attempt += 1;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/transport/timeout.ts
|
|
962
|
+
function createTimeoutSignal(timeoutMs, userSignal) {
|
|
963
|
+
if (!timeoutMs && !userSignal) {
|
|
964
|
+
const controller2 = new AbortController();
|
|
965
|
+
return { signal: controller2.signal, cancel: () => void 0 };
|
|
966
|
+
}
|
|
967
|
+
const controller = new AbortController();
|
|
968
|
+
let timerId;
|
|
969
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
970
|
+
timerId = setTimeout(() => {
|
|
971
|
+
controller.abort(
|
|
972
|
+
new OllamaTimeoutError(`Request timed out after ${timeoutMs}ms`, { timeoutMs })
|
|
973
|
+
);
|
|
974
|
+
}, timeoutMs);
|
|
975
|
+
}
|
|
976
|
+
const onUserAbort = () => {
|
|
977
|
+
if (timerId) clearTimeout(timerId);
|
|
978
|
+
controller.abort(userSignal?.reason);
|
|
979
|
+
};
|
|
980
|
+
if (userSignal) {
|
|
981
|
+
if (userSignal.aborted) {
|
|
982
|
+
if (timerId) clearTimeout(timerId);
|
|
983
|
+
controller.abort(userSignal.reason);
|
|
984
|
+
} else {
|
|
985
|
+
userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const cancel = () => {
|
|
989
|
+
if (timerId) clearTimeout(timerId);
|
|
990
|
+
if (userSignal) {
|
|
991
|
+
userSignal.removeEventListener("abort", onUserAbort);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
return { signal: controller.signal, cancel };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/models-client.ts
|
|
998
|
+
var ModelsClient = class {
|
|
999
|
+
constructor(runner) {
|
|
1000
|
+
this.runner = runner;
|
|
1001
|
+
}
|
|
1002
|
+
runner;
|
|
1003
|
+
list() {
|
|
1004
|
+
return this.runner((http) => listAvailableModels(http), { singleEndpoint: true });
|
|
1005
|
+
}
|
|
1006
|
+
show(request) {
|
|
1007
|
+
return this.runner(
|
|
1008
|
+
(http, signal) => http.request({ path: "/api/show", body: request, signal }),
|
|
1009
|
+
{ ...request, singleEndpoint: true }
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
async pull(request) {
|
|
1013
|
+
if (request.stream) {
|
|
1014
|
+
return this.runner(
|
|
1015
|
+
async (http, signal) => {
|
|
1016
|
+
const stream = await http.requestStream({
|
|
1017
|
+
path: "/api/pull",
|
|
1018
|
+
body: { ...request, stream: true },
|
|
1019
|
+
signal
|
|
1020
|
+
});
|
|
1021
|
+
return normalizeProgressStream(stream);
|
|
1022
|
+
},
|
|
1023
|
+
{ ...request, singleEndpoint: true }
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
return this.runner(
|
|
1027
|
+
(http, signal) => http.request({
|
|
1028
|
+
path: "/api/pull",
|
|
1029
|
+
body: { ...request, stream: false },
|
|
1030
|
+
signal
|
|
1031
|
+
}),
|
|
1032
|
+
{ ...request, singleEndpoint: true }
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
async push(request) {
|
|
1036
|
+
if (request.stream) {
|
|
1037
|
+
return this.runner(
|
|
1038
|
+
async (http, signal) => {
|
|
1039
|
+
const stream = await http.requestStream({
|
|
1040
|
+
path: "/api/push",
|
|
1041
|
+
body: { ...request, stream: true },
|
|
1042
|
+
signal
|
|
1043
|
+
});
|
|
1044
|
+
return normalizeProgressStream(stream);
|
|
1045
|
+
},
|
|
1046
|
+
{ ...request, singleEndpoint: true }
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
return this.runner(
|
|
1050
|
+
(http, signal) => http.request({
|
|
1051
|
+
path: "/api/push",
|
|
1052
|
+
body: { ...request, stream: false },
|
|
1053
|
+
signal
|
|
1054
|
+
}),
|
|
1055
|
+
{ ...request, singleEndpoint: true }
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
async create(request) {
|
|
1059
|
+
if (request.stream) {
|
|
1060
|
+
return this.runner(
|
|
1061
|
+
async (http, signal) => {
|
|
1062
|
+
const stream = await http.requestStream({
|
|
1063
|
+
path: "/api/create",
|
|
1064
|
+
body: { ...request, stream: true },
|
|
1065
|
+
signal
|
|
1066
|
+
});
|
|
1067
|
+
return normalizeProgressStream(stream);
|
|
1068
|
+
},
|
|
1069
|
+
{ ...request, singleEndpoint: true }
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return this.runner(
|
|
1073
|
+
(http, signal) => http.request({
|
|
1074
|
+
path: "/api/create",
|
|
1075
|
+
body: { ...request, stream: false },
|
|
1076
|
+
signal
|
|
1077
|
+
}),
|
|
1078
|
+
{ ...request, singleEndpoint: true }
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
delete(request) {
|
|
1082
|
+
return this.runner(
|
|
1083
|
+
(http, signal) => http.request({
|
|
1084
|
+
path: "/api/delete",
|
|
1085
|
+
method: "DELETE",
|
|
1086
|
+
body: request,
|
|
1087
|
+
signal
|
|
1088
|
+
}),
|
|
1089
|
+
{ ...request, singleEndpoint: true }
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
copy(request) {
|
|
1093
|
+
return this.runner(
|
|
1094
|
+
(http, signal) => http.request({ path: "/api/copy", body: request, signal }),
|
|
1095
|
+
{ ...request, singleEndpoint: true }
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
ps() {
|
|
1099
|
+
return this.runner((http) => http.request({ path: "/api/ps", method: "GET" }), {
|
|
1100
|
+
singleEndpoint: true
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
version() {
|
|
1104
|
+
return this.runner(
|
|
1105
|
+
(http) => http.request({ path: "/api/version", method: "GET" }),
|
|
1106
|
+
{ singleEndpoint: true }
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
async createBlob(digest, data) {
|
|
1110
|
+
await this.runner(
|
|
1111
|
+
(http, signal) => http.request({
|
|
1112
|
+
path: `/api/blobs/${encodeURIComponent(digest)}`,
|
|
1113
|
+
method: "POST",
|
|
1114
|
+
rawBody: data,
|
|
1115
|
+
signal
|
|
1116
|
+
}),
|
|
1117
|
+
{ singleEndpoint: true }
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
async checkBlob(digest) {
|
|
1121
|
+
try {
|
|
1122
|
+
await this.runner(
|
|
1123
|
+
(http, signal) => http.request({
|
|
1124
|
+
path: `/api/blobs/${encodeURIComponent(digest)}`,
|
|
1125
|
+
method: "HEAD",
|
|
1126
|
+
signal
|
|
1127
|
+
}),
|
|
1128
|
+
{ singleEndpoint: true }
|
|
1129
|
+
);
|
|
1130
|
+
return true;
|
|
1131
|
+
} catch {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// src/integrations/openai.ts
|
|
1138
|
+
var OpenAICompatClient = class {
|
|
1139
|
+
constructor(http) {
|
|
1140
|
+
this.http = http;
|
|
1141
|
+
}
|
|
1142
|
+
http;
|
|
1143
|
+
async createChatCompletion(request, signal) {
|
|
1144
|
+
return this.http.request({
|
|
1145
|
+
path: "/v1/chat/completions",
|
|
1146
|
+
body: request,
|
|
1147
|
+
signal
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async chatCompletions(request, signal) {
|
|
1151
|
+
return this.createChatCompletion(request, signal);
|
|
1152
|
+
}
|
|
1153
|
+
async listModels(signal) {
|
|
1154
|
+
return this.http.request({
|
|
1155
|
+
path: "/v1/models",
|
|
1156
|
+
method: "GET",
|
|
1157
|
+
signal
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/integrations/anthropic.ts
|
|
1163
|
+
var AnthropicCompatClient = class {
|
|
1164
|
+
constructor(http) {
|
|
1165
|
+
this.http = http;
|
|
1166
|
+
}
|
|
1167
|
+
http;
|
|
1168
|
+
async createMessage(request, signal) {
|
|
1169
|
+
return this.http.request({
|
|
1170
|
+
path: "/v1/messages",
|
|
1171
|
+
body: request,
|
|
1172
|
+
signal
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
async messages(request, signal) {
|
|
1176
|
+
return this.createMessage(request, signal);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// src/client.ts
|
|
1181
|
+
var OllamaClient = class {
|
|
1182
|
+
registry;
|
|
1183
|
+
models;
|
|
1184
|
+
retryConfig;
|
|
1185
|
+
timeoutMs;
|
|
1186
|
+
failoverCodes;
|
|
1187
|
+
fetchImpl;
|
|
1188
|
+
logger;
|
|
1189
|
+
constructor(config = {}) {
|
|
1190
|
+
const resolvedApiKey = resolveApiKey(config.apiKey);
|
|
1191
|
+
const endpoints = config.endpoints ?? [
|
|
1192
|
+
{
|
|
1193
|
+
name: "default",
|
|
1194
|
+
baseUrl: resolveBaseUrl(config.baseUrl),
|
|
1195
|
+
...resolvedApiKey !== void 0 ? { apiKey: resolvedApiKey } : {},
|
|
1196
|
+
...config.headers !== void 0 ? { headers: config.headers } : {}
|
|
1197
|
+
}
|
|
1198
|
+
];
|
|
1199
|
+
this.registry = new EndpointRegistry(endpoints, config.endpointHealth);
|
|
1200
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1201
|
+
this.failoverCodes = new Set(config.failoverOn ?? DEFAULT_FAILOVER_CODES);
|
|
1202
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
1203
|
+
this.logger = config.logger ?? (config.debug ? createConsoleLogger() : NOOP_LOGGER);
|
|
1204
|
+
this.retryConfig = typeof config.retries === "number" ? { ...DEFAULT_RETRY_CONFIG, maxRetries: config.retries } : { ...DEFAULT_RETRY_CONFIG, ...config.retries };
|
|
1205
|
+
this.models = new ModelsClient((op, opts) => this.executeWithFailover(op, opts));
|
|
1206
|
+
}
|
|
1207
|
+
get modelsClient() {
|
|
1208
|
+
return this.models;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Fail-fast guard for `format` (structured output) requests: throws before any network
|
|
1212
|
+
* call if the candidate endpoint is inferred as Ollama Cloud, which does not currently
|
|
1213
|
+
* support structured outputs (see `ModelCapabilities.supportsStructuredOutputRequest`).
|
|
1214
|
+
* `unsupported_capability` is in `DEFAULT_FAILOVER_CODES`, so in a multi-endpoint setup
|
|
1215
|
+
* this causes failover to the next candidate rather than failing the whole request.
|
|
1216
|
+
*/
|
|
1217
|
+
assertStructuredOutputSupported(baseUrl, model) {
|
|
1218
|
+
if (inferRuntimeMode(baseUrl) === "cloud") {
|
|
1219
|
+
throw new OllamaUnsupportedCapabilityError(
|
|
1220
|
+
`Structured output ("format") requests are not supported against Ollama Cloud endpoints (model "${model}" via ${baseUrl}). This is a known Ollama Cloud limitation, not a bug in this SDK.`,
|
|
1221
|
+
{ capability: "structuredOutputRequest" }
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async executeWithFailover(operation, options) {
|
|
1226
|
+
const timeout = createTimeoutSignal(options?.timeoutMs ?? this.timeoutMs, options?.signal);
|
|
1227
|
+
try {
|
|
1228
|
+
const allCandidates = this.registry.candidates();
|
|
1229
|
+
const candidates = options?.singleEndpoint ? allCandidates.slice(0, 1) : allCandidates;
|
|
1230
|
+
let lastError;
|
|
1231
|
+
for (const [attemptIndex, endpoint] of candidates.entries()) {
|
|
1232
|
+
this.logger.debug(`Executing on endpoint "${endpoint.name}" (${endpoint.baseUrl})`);
|
|
1233
|
+
const http = new HttpClient({
|
|
1234
|
+
baseUrl: endpoint.baseUrl,
|
|
1235
|
+
...endpoint.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
|
|
1236
|
+
...endpoint.headers !== void 0 ? { headers: endpoint.headers } : {},
|
|
1237
|
+
fetch: this.fetchImpl
|
|
1238
|
+
});
|
|
1239
|
+
try {
|
|
1240
|
+
const result = await withSpan(
|
|
1241
|
+
"ollama.endpoint.attempt",
|
|
1242
|
+
{
|
|
1243
|
+
[ATTR_OLLAMA_ENDPOINT_NAME]: endpoint.name,
|
|
1244
|
+
[ATTR_OLLAMA_ENDPOINT_ATTEMPT]: attemptIndex
|
|
1245
|
+
},
|
|
1246
|
+
() => withRetry(() => operation(http, timeout.signal), this.retryConfig)
|
|
1247
|
+
);
|
|
1248
|
+
this.registry.reportSuccess(endpoint.name);
|
|
1249
|
+
return result;
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1252
|
+
lastError = error;
|
|
1253
|
+
this.registry.reportFailure(endpoint.name);
|
|
1254
|
+
this.logger.warn(`Failed on "${endpoint.name}": ${error.message}`);
|
|
1255
|
+
if (!(error instanceof OllamaClientError && this.failoverCodes.has(error.code)))
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
throw lastError ?? new Error("No healthy Ollama endpoints available");
|
|
1260
|
+
} finally {
|
|
1261
|
+
timeout.cancel();
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async chat(req) {
|
|
1265
|
+
if (req.stream) {
|
|
1266
|
+
return this.executeWithFailover(async (http, signal) => {
|
|
1267
|
+
if (req.format !== void 0) this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
1268
|
+
const stream = await http.requestStream({
|
|
1269
|
+
path: "/api/chat",
|
|
1270
|
+
body: { ...req, stream: true },
|
|
1271
|
+
signal
|
|
1272
|
+
});
|
|
1273
|
+
return normalizeChatStream(stream);
|
|
1274
|
+
}, req);
|
|
1275
|
+
}
|
|
1276
|
+
return withSpan(
|
|
1277
|
+
`chat ${req.model}`,
|
|
1278
|
+
{
|
|
1279
|
+
[ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_OLLAMA,
|
|
1280
|
+
[ATTR_GEN_AI_OPERATION_NAME]: "chat",
|
|
1281
|
+
[ATTR_GEN_AI_REQUEST_MODEL]: req.model
|
|
1282
|
+
},
|
|
1283
|
+
async (span) => {
|
|
1284
|
+
const rawRes = await this.executeWithFailover((http, signal) => {
|
|
1285
|
+
if (req.format !== void 0)
|
|
1286
|
+
this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
1287
|
+
return http.request({
|
|
1288
|
+
path: "/api/chat",
|
|
1289
|
+
body: { ...req, stream: false },
|
|
1290
|
+
signal
|
|
1291
|
+
});
|
|
1292
|
+
}, req);
|
|
1293
|
+
const toolCalls = ensureToolCallIds(rawRes.message.tool_calls);
|
|
1294
|
+
const res = toolCalls === rawRes.message.tool_calls ? rawRes : { ...rawRes, message: { ...rawRes.message, tool_calls: toolCalls } };
|
|
1295
|
+
span?.setAttributes({
|
|
1296
|
+
[ATTR_GEN_AI_RESPONSE_MODEL]: res.model,
|
|
1297
|
+
...res.prompt_eval_count !== void 0 ? { [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: res.prompt_eval_count } : {},
|
|
1298
|
+
...res.eval_count !== void 0 ? { [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: res.eval_count } : {}
|
|
1299
|
+
});
|
|
1300
|
+
return res;
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
chatStream(req) {
|
|
1305
|
+
return this.chat({ ...req, stream: true });
|
|
1306
|
+
}
|
|
1307
|
+
async chatText(req) {
|
|
1308
|
+
const res = await this.chat({ ...req, stream: false });
|
|
1309
|
+
return res.message.content;
|
|
1310
|
+
}
|
|
1311
|
+
async chatWithSchema(req, schema) {
|
|
1312
|
+
const res = await this.chat({ ...req, format: zodToJsonSchema(schema), stream: false });
|
|
1313
|
+
return parseStructuredOutput(res.message.content, schema);
|
|
1314
|
+
}
|
|
1315
|
+
async generate(req) {
|
|
1316
|
+
if (req.stream) {
|
|
1317
|
+
return this.executeWithFailover(async (http, signal) => {
|
|
1318
|
+
if (req.format !== void 0) this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
1319
|
+
const stream = await http.requestStream({
|
|
1320
|
+
path: "/api/generate",
|
|
1321
|
+
body: { ...req, stream: true },
|
|
1322
|
+
signal
|
|
1323
|
+
});
|
|
1324
|
+
return normalizeGenerateStream(stream);
|
|
1325
|
+
}, req);
|
|
1326
|
+
}
|
|
1327
|
+
return withSpan(
|
|
1328
|
+
`text_completion ${req.model}`,
|
|
1329
|
+
{
|
|
1330
|
+
[ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_OLLAMA,
|
|
1331
|
+
[ATTR_GEN_AI_OPERATION_NAME]: "text_completion",
|
|
1332
|
+
[ATTR_GEN_AI_REQUEST_MODEL]: req.model
|
|
1333
|
+
},
|
|
1334
|
+
async (span) => {
|
|
1335
|
+
const res = await this.executeWithFailover((http, signal) => {
|
|
1336
|
+
if (req.format !== void 0)
|
|
1337
|
+
this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
1338
|
+
return http.request({
|
|
1339
|
+
path: "/api/generate",
|
|
1340
|
+
body: { ...req, stream: false },
|
|
1341
|
+
signal
|
|
1342
|
+
});
|
|
1343
|
+
}, req);
|
|
1344
|
+
span?.setAttributes({
|
|
1345
|
+
[ATTR_GEN_AI_RESPONSE_MODEL]: res.model,
|
|
1346
|
+
...res.prompt_eval_count !== void 0 ? { [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: res.prompt_eval_count } : {},
|
|
1347
|
+
...res.eval_count !== void 0 ? { [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: res.eval_count } : {}
|
|
1348
|
+
});
|
|
1349
|
+
return res;
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
generateStream(req) {
|
|
1354
|
+
return this.generate({ ...req, stream: true });
|
|
1355
|
+
}
|
|
1356
|
+
async generateText(req) {
|
|
1357
|
+
const res = await this.generate({ ...req, stream: false });
|
|
1358
|
+
return res.response;
|
|
1359
|
+
}
|
|
1360
|
+
async generateWithSchema(req, schema) {
|
|
1361
|
+
const res = await this.generate({ ...req, format: zodToJsonSchema(schema), stream: false });
|
|
1362
|
+
return parseStructuredOutput(res.response, schema);
|
|
1363
|
+
}
|
|
1364
|
+
// --- Embeddings ---
|
|
1365
|
+
embed(req) {
|
|
1366
|
+
return this.executeWithFailover(
|
|
1367
|
+
(http, signal) => http.request({ path: "/api/embed", body: req, signal }),
|
|
1368
|
+
req
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
async embedText(model, input) {
|
|
1372
|
+
const res = await this.embed({ model, input });
|
|
1373
|
+
return res.embeddings;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* @deprecated Ollama's `/api/embeddings` endpoint has been superseded by `/api/embed`
|
|
1377
|
+
* (exposed here as {@link OllamaClient.embed}), which additionally supports batch
|
|
1378
|
+
* input. Kept for compatibility with existing callers; new code should use `embed`.
|
|
1379
|
+
*/
|
|
1380
|
+
embeddings(req) {
|
|
1381
|
+
return this.executeWithFailover(
|
|
1382
|
+
(http, signal) => http.request({ path: "/api/embeddings", body: req, signal }),
|
|
1383
|
+
req
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
// --- Model Operations (Delegated to ModelsClient) ---
|
|
1387
|
+
listModels = () => this.models.list();
|
|
1388
|
+
showModel = (req) => this.models.show(req);
|
|
1389
|
+
pullModel = (r) => this.models.pull(r);
|
|
1390
|
+
pushModel = (r) => this.models.push(r);
|
|
1391
|
+
createModel = (r) => this.models.create(r);
|
|
1392
|
+
deleteModel = (req) => this.models.delete(req);
|
|
1393
|
+
copyModel = (req) => this.models.copy(req);
|
|
1394
|
+
ps = () => this.models.ps();
|
|
1395
|
+
version = () => this.models.version();
|
|
1396
|
+
createBlob = (digest, data) => this.models.createBlob(digest, data);
|
|
1397
|
+
checkBlob = (digest) => this.models.checkBlob(digest);
|
|
1398
|
+
// --- Web Endpoints ---
|
|
1399
|
+
webSearch(req) {
|
|
1400
|
+
return this.executeWithFailover(
|
|
1401
|
+
(http, signal) => http.request({ path: "/api/websearch", body: req, signal }),
|
|
1402
|
+
req
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
webFetch(req) {
|
|
1406
|
+
return this.executeWithFailover(
|
|
1407
|
+
(http, signal) => http.request({ path: "/api/webfetch", body: req, signal }),
|
|
1408
|
+
req
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
// --- Capabilities & Health ---
|
|
1412
|
+
capabilities(model) {
|
|
1413
|
+
return this.executeWithFailover((http) => detectModelCapabilities(http, model), {
|
|
1414
|
+
singleEndpoint: true
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
runtimeMode() {
|
|
1418
|
+
const ep = this.registry.candidates()[0];
|
|
1419
|
+
return inferRuntimeMode(ep?.baseUrl ?? DEFAULT_BASE_URL);
|
|
1420
|
+
}
|
|
1421
|
+
healthCheck() {
|
|
1422
|
+
return Promise.all(this.registry.list().map((ep) => checkEndpointHealth(ep, this.fetchImpl)));
|
|
1423
|
+
}
|
|
1424
|
+
endpointStatus() {
|
|
1425
|
+
return this.registry.status();
|
|
1426
|
+
}
|
|
1427
|
+
// --- Compatibility Adapters ---
|
|
1428
|
+
get openai() {
|
|
1429
|
+
const ep = this.registry.candidates()[0];
|
|
1430
|
+
return new OpenAICompatClient(
|
|
1431
|
+
new HttpClient({
|
|
1432
|
+
baseUrl: ep?.baseUrl ?? DEFAULT_BASE_URL,
|
|
1433
|
+
apiKey: ep?.apiKey,
|
|
1434
|
+
headers: ep?.headers,
|
|
1435
|
+
fetch: this.fetchImpl
|
|
1436
|
+
})
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
get anthropic() {
|
|
1440
|
+
const ep = this.registry.candidates()[0];
|
|
1441
|
+
return new AnthropicCompatClient(
|
|
1442
|
+
new HttpClient({
|
|
1443
|
+
baseUrl: ep?.baseUrl ?? DEFAULT_BASE_URL,
|
|
1444
|
+
apiKey: ep?.apiKey,
|
|
1445
|
+
headers: ep?.headers,
|
|
1446
|
+
fetch: this.fetchImpl
|
|
1447
|
+
})
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
// src/middleware.ts
|
|
1453
|
+
function composeMiddleware(middlewares, finalHandler) {
|
|
1454
|
+
return function runPipeline(initialRequest) {
|
|
1455
|
+
let index = -1;
|
|
1456
|
+
function dispatch(i, currentRequest) {
|
|
1457
|
+
if (i <= index) {
|
|
1458
|
+
return Promise.reject(new Error("next() called multiple times in middleware"));
|
|
1459
|
+
}
|
|
1460
|
+
index = i;
|
|
1461
|
+
const fn = middlewares[i];
|
|
1462
|
+
if (i === middlewares.length || !fn) {
|
|
1463
|
+
return finalHandler(currentRequest);
|
|
1464
|
+
}
|
|
1465
|
+
return fn({
|
|
1466
|
+
request: currentRequest,
|
|
1467
|
+
next: () => dispatch(i + 1, currentRequest)
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
return dispatch(0, initialRequest);
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/streaming/adapters.ts
|
|
1475
|
+
function toTextStream(stream) {
|
|
1476
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
1477
|
+
return new ReadableStream({
|
|
1478
|
+
async pull(controller) {
|
|
1479
|
+
try {
|
|
1480
|
+
while (true) {
|
|
1481
|
+
const { done, value } = await iterator.next();
|
|
1482
|
+
if (done) {
|
|
1483
|
+
controller.close();
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (value.type === "token" && value.data.delta) {
|
|
1487
|
+
controller.enqueue(value.data.delta);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
controller.error(err);
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
cancel() {
|
|
1496
|
+
stream.abort();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
function toDataStream(stream) {
|
|
1501
|
+
const textStream = toTextStream(stream);
|
|
1502
|
+
const reader = textStream.getReader();
|
|
1503
|
+
const encoder = new TextEncoder();
|
|
1504
|
+
return new ReadableStream({
|
|
1505
|
+
async pull(controller) {
|
|
1506
|
+
try {
|
|
1507
|
+
const { done, value } = await reader.read();
|
|
1508
|
+
if (done) {
|
|
1509
|
+
controller.close();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
controller.enqueue(encoder.encode(value));
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
controller.error(err);
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
cancel() {
|
|
1518
|
+
reader.cancel();
|
|
1519
|
+
stream.abort();
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
function toResponse(stream, init = {}) {
|
|
1524
|
+
const body = toDataStream(stream);
|
|
1525
|
+
const headers = new Headers(init.headers);
|
|
1526
|
+
if (!headers.has("Content-Type")) {
|
|
1527
|
+
headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
1528
|
+
}
|
|
1529
|
+
return new Response(body, { ...init, headers });
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/tools/define-tool.ts
|
|
1533
|
+
function defineTool(options) {
|
|
1534
|
+
const jsonSchema = zodToJsonSchema(options.schema);
|
|
1535
|
+
const properties = jsonSchema["properties"] ?? {};
|
|
1536
|
+
const required = jsonSchema["required"] ?? [];
|
|
1537
|
+
return {
|
|
1538
|
+
name: options.name,
|
|
1539
|
+
description: options.description,
|
|
1540
|
+
schema: options.schema,
|
|
1541
|
+
execute: options.execute,
|
|
1542
|
+
timeoutMs: options.timeoutMs,
|
|
1543
|
+
definition: {
|
|
1544
|
+
type: "function",
|
|
1545
|
+
function: {
|
|
1546
|
+
name: options.name,
|
|
1547
|
+
description: options.description,
|
|
1548
|
+
parameters: {
|
|
1549
|
+
type: "object",
|
|
1550
|
+
properties,
|
|
1551
|
+
...required.length > 0 ? { required } : {}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/tools/registry.ts
|
|
1559
|
+
var ToolRegistry = class {
|
|
1560
|
+
tools = /* @__PURE__ */ new Map();
|
|
1561
|
+
onError;
|
|
1562
|
+
defaultTimeoutMs;
|
|
1563
|
+
maxConcurrency;
|
|
1564
|
+
maxOutputChars;
|
|
1565
|
+
constructor(toolsOrOptions) {
|
|
1566
|
+
if (Array.isArray(toolsOrOptions)) {
|
|
1567
|
+
this.registerMany(toolsOrOptions);
|
|
1568
|
+
} else if (toolsOrOptions !== void 0) {
|
|
1569
|
+
const opts = toolsOrOptions;
|
|
1570
|
+
this.onError = opts.onError;
|
|
1571
|
+
this.defaultTimeoutMs = opts.timeoutMs;
|
|
1572
|
+
this.maxConcurrency = opts.maxConcurrency;
|
|
1573
|
+
this.maxOutputChars = opts.maxOutputChars;
|
|
1574
|
+
if (opts.tools) {
|
|
1575
|
+
this.registerMany(opts.tools);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
register(tool) {
|
|
1580
|
+
this.tools.set(tool.name, tool);
|
|
1581
|
+
return this;
|
|
1582
|
+
}
|
|
1583
|
+
registerMany(tools) {
|
|
1584
|
+
for (const tool of tools) {
|
|
1585
|
+
this.register(tool);
|
|
1586
|
+
}
|
|
1587
|
+
return this;
|
|
1588
|
+
}
|
|
1589
|
+
get(name) {
|
|
1590
|
+
return this.tools.get(name);
|
|
1591
|
+
}
|
|
1592
|
+
definitions() {
|
|
1593
|
+
return Array.from(this.tools.values()).map((t) => t.definition);
|
|
1594
|
+
}
|
|
1595
|
+
async executeToolCall(toolCall, ctx = {}) {
|
|
1596
|
+
const { name } = toolCall.function;
|
|
1597
|
+
return withSpan(
|
|
1598
|
+
`execute_tool ${name}`,
|
|
1599
|
+
{ [ATTR_GEN_AI_TOOL_NAME]: name, [ATTR_GEN_AI_TOOL_CALL_ID]: toolCall.id },
|
|
1600
|
+
async (span) => {
|
|
1601
|
+
const result = await this.executeToolCallUninstrumented(toolCall, ctx);
|
|
1602
|
+
if (!result.success) {
|
|
1603
|
+
await setSpanError(span, result.error);
|
|
1604
|
+
}
|
|
1605
|
+
return result;
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
async executeToolCallUninstrumented(toolCall, ctx) {
|
|
1610
|
+
const { name, arguments: args } = toolCall.function;
|
|
1611
|
+
const tool = this.tools.get(name);
|
|
1612
|
+
if (!tool) {
|
|
1613
|
+
const err = new OllamaNotFoundError(`Tool "${name}" is not registered in this registry`);
|
|
1614
|
+
return this.handleExecutionError(err, toolCall);
|
|
1615
|
+
}
|
|
1616
|
+
const parseResult = tool.schema.safeParse(args);
|
|
1617
|
+
if (!parseResult.success) {
|
|
1618
|
+
const err = new OllamaToolValidationError(`Tool "${name}" arguments validation failed`, {
|
|
1619
|
+
toolName: name,
|
|
1620
|
+
issues: parseResult.error.issues,
|
|
1621
|
+
cause: parseResult.error
|
|
1622
|
+
});
|
|
1623
|
+
return this.handleExecutionError(err, toolCall);
|
|
1624
|
+
}
|
|
1625
|
+
try {
|
|
1626
|
+
const result = await this.runWithTimeout(tool, parseResult.data, ctx, name);
|
|
1627
|
+
const outputString = this.truncateOutput(
|
|
1628
|
+
typeof result === "string" ? result : JSON.stringify(result)
|
|
1629
|
+
);
|
|
1630
|
+
return {
|
|
1631
|
+
toolName: name,
|
|
1632
|
+
toolCallId: toolCall.id,
|
|
1633
|
+
success: true,
|
|
1634
|
+
result,
|
|
1635
|
+
outputString
|
|
1636
|
+
};
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1639
|
+
return this.handleExecutionError(error, toolCall);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Executes every call in `toolCalls` and returns results in the same order, so results
|
|
1644
|
+
* are always correlatable by array position regardless of `id`. Each result also echoes
|
|
1645
|
+
* its call's `toolCallId` for callers who'd rather match by ID — see the {@link ToolCall}
|
|
1646
|
+
* docs for how that id is produced. Runs fully in parallel (`Promise.all`) unless
|
|
1647
|
+
* `maxConcurrency` was configured, in which case a bounded worker pool is used instead.
|
|
1648
|
+
*/
|
|
1649
|
+
async executeToolCalls(toolCalls, ctx = {}) {
|
|
1650
|
+
const limit = this.maxConcurrency;
|
|
1651
|
+
if (!limit || limit >= toolCalls.length) {
|
|
1652
|
+
return Promise.all(toolCalls.map((tc) => this.executeToolCall(tc, ctx)));
|
|
1653
|
+
}
|
|
1654
|
+
const results = new Array(toolCalls.length);
|
|
1655
|
+
let nextIndex = 0;
|
|
1656
|
+
const worker = async () => {
|
|
1657
|
+
for (let i = nextIndex++; i < toolCalls.length; i = nextIndex++) {
|
|
1658
|
+
results[i] = await this.executeToolCall(toolCalls[i], ctx);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
1662
|
+
return results;
|
|
1663
|
+
}
|
|
1664
|
+
async runWithTimeout(tool, params, ctx, toolName) {
|
|
1665
|
+
const timeoutMs = tool.timeoutMs ?? this.defaultTimeoutMs;
|
|
1666
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
1667
|
+
return tool.execute(params, ctx);
|
|
1668
|
+
}
|
|
1669
|
+
const controller = new AbortController();
|
|
1670
|
+
const onParentAbort = () => controller.abort(ctx.signal?.reason);
|
|
1671
|
+
if (ctx.signal) {
|
|
1672
|
+
if (ctx.signal.aborted) {
|
|
1673
|
+
controller.abort(ctx.signal.reason);
|
|
1674
|
+
} else {
|
|
1675
|
+
ctx.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
let timedOut = false;
|
|
1679
|
+
const timer = setTimeout(() => {
|
|
1680
|
+
timedOut = true;
|
|
1681
|
+
controller.abort();
|
|
1682
|
+
}, timeoutMs);
|
|
1683
|
+
try {
|
|
1684
|
+
const executePromise = Promise.resolve(
|
|
1685
|
+
tool.execute(params, { ...ctx, signal: controller.signal })
|
|
1686
|
+
);
|
|
1687
|
+
executePromise.catch(() => void 0);
|
|
1688
|
+
const abortRejection = () => timedOut ? new OllamaToolTimeoutError(`Tool "${toolName}" timed out after ${timeoutMs}ms`, {
|
|
1689
|
+
toolName,
|
|
1690
|
+
timeoutMs
|
|
1691
|
+
}) : ctx.signal?.reason ?? new Error("Tool execution aborted");
|
|
1692
|
+
return await Promise.race([
|
|
1693
|
+
executePromise,
|
|
1694
|
+
controller.signal.aborted ? Promise.reject(abortRejection()) : new Promise((_resolve, reject) => {
|
|
1695
|
+
controller.signal.addEventListener("abort", () => reject(abortRejection()), {
|
|
1696
|
+
once: true
|
|
1697
|
+
});
|
|
1698
|
+
})
|
|
1699
|
+
]);
|
|
1700
|
+
} finally {
|
|
1701
|
+
clearTimeout(timer);
|
|
1702
|
+
if (ctx.signal) {
|
|
1703
|
+
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
truncateOutput(output) {
|
|
1708
|
+
const limit = this.maxOutputChars;
|
|
1709
|
+
if (!limit || output.length <= limit) {
|
|
1710
|
+
return output;
|
|
1711
|
+
}
|
|
1712
|
+
return `${output.slice(0, limit)}
|
|
1713
|
+
\u2026 [truncated ${output.length - limit} of ${output.length} chars]`;
|
|
1714
|
+
}
|
|
1715
|
+
handleExecutionError(error, toolCall) {
|
|
1716
|
+
const fallbackMessage = this.onError ? this.onError(error, toolCall) : error.message;
|
|
1717
|
+
return {
|
|
1718
|
+
toolName: toolCall.function.name,
|
|
1719
|
+
toolCallId: toolCall.id,
|
|
1720
|
+
success: false,
|
|
1721
|
+
error,
|
|
1722
|
+
outputString: this.truncateOutput(fallbackMessage)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
function convertMcpDescriptorToTool(descriptor, mcpClient, namePrefix = "") {
|
|
1727
|
+
const toolName = `${namePrefix}${descriptor.name}`;
|
|
1728
|
+
const inputSchema = descriptor.inputSchema ?? { properties: {} };
|
|
1729
|
+
const properties = inputSchema["properties"] ?? {};
|
|
1730
|
+
const required = inputSchema["required"] ?? [];
|
|
1731
|
+
return {
|
|
1732
|
+
name: toolName,
|
|
1733
|
+
description: descriptor.description ?? "",
|
|
1734
|
+
schema: z.record(z.string(), z.unknown()),
|
|
1735
|
+
execute: async (args) => {
|
|
1736
|
+
try {
|
|
1737
|
+
const result = await mcpClient.callTool({
|
|
1738
|
+
name: descriptor.name,
|
|
1739
|
+
arguments: args
|
|
1740
|
+
});
|
|
1741
|
+
if (result.isError) {
|
|
1742
|
+
throw new OllamaMcpError(`MCP tool "${descriptor.name}" returned error`, {
|
|
1743
|
+
mcpMethod: "callTool",
|
|
1744
|
+
toolName: descriptor.name
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return result.content.map((b) => b.type === "text" && b.text ? b.text : `[${b.type}]`).join("\n");
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
if (err instanceof OllamaMcpError) throw err;
|
|
1750
|
+
throw new OllamaMcpError(`Failed calling MCP tool "${descriptor.name}"`, {
|
|
1751
|
+
mcpMethod: "callTool",
|
|
1752
|
+
toolName: descriptor.name,
|
|
1753
|
+
cause: err
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
definition: {
|
|
1758
|
+
type: "function",
|
|
1759
|
+
function: {
|
|
1760
|
+
name: toolName,
|
|
1761
|
+
description: descriptor.description ?? "",
|
|
1762
|
+
parameters: {
|
|
1763
|
+
type: "object",
|
|
1764
|
+
properties,
|
|
1765
|
+
...required.length > 0 ? { required } : {}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
async function loadMcpTools(mcpClient, options = {}) {
|
|
1772
|
+
try {
|
|
1773
|
+
const { tools } = await mcpClient.listTools();
|
|
1774
|
+
return tools.map((t) => convertMcpDescriptorToTool(t, mcpClient, options.namePrefix));
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
throw new OllamaMcpError("Failed listing MCP tools from client", {
|
|
1777
|
+
mcpMethod: "listTools",
|
|
1778
|
+
cause: err
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function registerMcpTools(registry, mcpClient, options = {}) {
|
|
1783
|
+
const tools = await loadMcpTools(mcpClient, options);
|
|
1784
|
+
registry.registerMany(tools);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/skills/frontmatter.ts
|
|
1788
|
+
function parseYamlValue(raw) {
|
|
1789
|
+
const trimmed = raw.trim();
|
|
1790
|
+
if (trimmed === "true") return true;
|
|
1791
|
+
if (trimmed === "false") return false;
|
|
1792
|
+
if (!Number.isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
|
|
1793
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1794
|
+
return trimmed.slice(1, -1);
|
|
1795
|
+
}
|
|
1796
|
+
return trimmed;
|
|
1797
|
+
}
|
|
1798
|
+
function parseYamlBlock(yamlText) {
|
|
1799
|
+
const result = {};
|
|
1800
|
+
const lines = yamlText.split("\n");
|
|
1801
|
+
let currentKey;
|
|
1802
|
+
let currentList;
|
|
1803
|
+
for (const line of lines) {
|
|
1804
|
+
const trimmed = line.trim();
|
|
1805
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1806
|
+
if (trimmed.startsWith("- ") && currentKey) {
|
|
1807
|
+
if (!currentList) {
|
|
1808
|
+
currentList = [];
|
|
1809
|
+
result[currentKey] = currentList;
|
|
1810
|
+
}
|
|
1811
|
+
currentList.push(parseYamlValue(trimmed.slice(2)));
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
currentList = void 0;
|
|
1815
|
+
const colonIndex = line.indexOf(":");
|
|
1816
|
+
if (colonIndex > 0) {
|
|
1817
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1818
|
+
const valStr = line.slice(colonIndex + 1).trim();
|
|
1819
|
+
currentKey = key;
|
|
1820
|
+
if (valStr.length > 0) {
|
|
1821
|
+
result[key] = parseYamlValue(valStr);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return result;
|
|
1826
|
+
}
|
|
1827
|
+
function parseFrontmatter(source, path = "") {
|
|
1828
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
1829
|
+
if (!match) {
|
|
1830
|
+
throw new OllamaSkillInvalidError("Missing YAML frontmatter delimiters (---)", {
|
|
1831
|
+
skillName: path || "unknown",
|
|
1832
|
+
path
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
const rawYaml = match[1] ?? "";
|
|
1836
|
+
const body = match[2]?.trim() ?? "";
|
|
1837
|
+
const parsed = parseYamlBlock(rawYaml);
|
|
1838
|
+
if (!parsed["name"] || typeof parsed["name"] !== "string") {
|
|
1839
|
+
throw new OllamaSkillInvalidError('Frontmatter missing required string "name"', {
|
|
1840
|
+
skillName: String(parsed["name"] ?? "unknown"),
|
|
1841
|
+
path
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
if (!parsed["description"] || typeof parsed["description"] !== "string") {
|
|
1845
|
+
throw new OllamaSkillInvalidError('Frontmatter missing required string "description"', {
|
|
1846
|
+
skillName: parsed["name"],
|
|
1847
|
+
path
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
return {
|
|
1851
|
+
frontmatter: parsed,
|
|
1852
|
+
body
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/skills/compose.ts
|
|
1857
|
+
function applySkill(skill, options) {
|
|
1858
|
+
const systemPrompt = `[Skill: ${skill.name}]
|
|
1859
|
+
${skill.description}
|
|
1860
|
+
|
|
1861
|
+
${skill.body}`;
|
|
1862
|
+
const messages = [...options.messages];
|
|
1863
|
+
const systemIndex = messages.findIndex((m) => m.role === "system");
|
|
1864
|
+
if (systemIndex >= 0) {
|
|
1865
|
+
const existing = messages[systemIndex];
|
|
1866
|
+
if (existing) {
|
|
1867
|
+
messages[systemIndex] = {
|
|
1868
|
+
role: "system",
|
|
1869
|
+
content: `${existing.content}
|
|
1870
|
+
|
|
1871
|
+
${systemPrompt}`
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
} else {
|
|
1875
|
+
messages.unshift({ role: "system", content: systemPrompt });
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
messages,
|
|
1879
|
+
...options.tools !== void 0 ? { tools: options.tools } : {}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/agent/agent.ts
|
|
1884
|
+
var Agent = class {
|
|
1885
|
+
client;
|
|
1886
|
+
tools;
|
|
1887
|
+
maxIterations;
|
|
1888
|
+
hooks;
|
|
1889
|
+
constructor(client, config = {}) {
|
|
1890
|
+
this.client = client;
|
|
1891
|
+
this.tools = config.tools;
|
|
1892
|
+
this.maxIterations = config.maxIterations ?? 10;
|
|
1893
|
+
this.hooks = config.hooks;
|
|
1894
|
+
}
|
|
1895
|
+
run(input) {
|
|
1896
|
+
return withSpan(
|
|
1897
|
+
`invoke_agent ${input.model}`,
|
|
1898
|
+
{
|
|
1899
|
+
[ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_OLLAMA,
|
|
1900
|
+
[ATTR_GEN_AI_OPERATION_NAME]: "invoke_agent",
|
|
1901
|
+
[ATTR_GEN_AI_REQUEST_MODEL]: input.model,
|
|
1902
|
+
[ATTR_OLLAMA_AGENT_MAX_ITERATIONS]: this.maxIterations
|
|
1903
|
+
},
|
|
1904
|
+
() => this.runLoop(input)
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
async runLoop(input) {
|
|
1908
|
+
const history = [...input.messages];
|
|
1909
|
+
const turns = [];
|
|
1910
|
+
const toolDefs = this.tools?.definitions();
|
|
1911
|
+
for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
|
|
1912
|
+
const outcome = await withSpan(
|
|
1913
|
+
"ollama.agent.turn",
|
|
1914
|
+
{ [ATTR_OLLAMA_AGENT_ITERATION]: iteration },
|
|
1915
|
+
async () => {
|
|
1916
|
+
this.hooks?.onTurnStart?.(iteration);
|
|
1917
|
+
const response = await this.client.chat({
|
|
1918
|
+
model: input.model,
|
|
1919
|
+
messages: history,
|
|
1920
|
+
...toolDefs !== void 0 ? { tools: toolDefs } : {},
|
|
1921
|
+
...input.options !== void 0 ? { options: input.options } : {},
|
|
1922
|
+
...input.think !== void 0 ? { think: input.think } : {},
|
|
1923
|
+
stream: false,
|
|
1924
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1925
|
+
});
|
|
1926
|
+
const rawToolCalls = response.message.tool_calls;
|
|
1927
|
+
const toolCalls = ensureToolCallIds(rawToolCalls);
|
|
1928
|
+
const assistantMessage = toolCalls === rawToolCalls ? response.message : { ...response.message, tool_calls: toolCalls };
|
|
1929
|
+
history.push(assistantMessage);
|
|
1930
|
+
if (!toolCalls || toolCalls.length === 0 || !this.tools) {
|
|
1931
|
+
const finalTurn = { iteration, message: assistantMessage };
|
|
1932
|
+
turns.push(finalTurn);
|
|
1933
|
+
this.hooks?.onTurnEnd?.(finalTurn);
|
|
1934
|
+
return { done: true, finalMessage: assistantMessage };
|
|
1935
|
+
}
|
|
1936
|
+
for (const tc of toolCalls) {
|
|
1937
|
+
this.hooks?.onToolCallStart?.(tc);
|
|
1938
|
+
}
|
|
1939
|
+
const toolResults = await this.tools.executeToolCalls(toolCalls, {
|
|
1940
|
+
signal: input.signal
|
|
1941
|
+
});
|
|
1942
|
+
for (const res of toolResults) {
|
|
1943
|
+
this.hooks?.onToolCallEnd?.(res);
|
|
1944
|
+
history.push({
|
|
1945
|
+
role: "tool",
|
|
1946
|
+
...res.toolCallId !== void 0 ? { tool_call_id: res.toolCallId } : {},
|
|
1947
|
+
content: res.outputString
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
const turn = { iteration, message: assistantMessage, toolCalls, toolResults };
|
|
1951
|
+
turns.push(turn);
|
|
1952
|
+
this.hooks?.onTurnEnd?.(turn);
|
|
1953
|
+
return { done: false };
|
|
1954
|
+
}
|
|
1955
|
+
);
|
|
1956
|
+
if (outcome.done) {
|
|
1957
|
+
return { finalMessage: outcome.finalMessage, turns, totalIterations: iteration };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
throw new OllamaAgentMaxIterationsError(
|
|
1961
|
+
`Agent exceeded max iterations (${this.maxIterations}) without converging`,
|
|
1962
|
+
{ maxIterations: this.maxIterations }
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
export { ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM, ATTR_GEN_AI_TOOL_CALL_ID, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_OLLAMA_AGENT_ITERATION, ATTR_OLLAMA_AGENT_MAX_ITERATIONS, ATTR_OLLAMA_ENDPOINT_ATTEMPT, ATTR_OLLAMA_ENDPOINT_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, Agent, AnthropicCompatClient, DEFAULT_BACKOFF, DEFAULT_BASE_URL, DEFAULT_FAILOVER_CODES, DEFAULT_RETRY_CONFIG, DEFAULT_TIMEOUT_MS, EndpointRegistry, GEN_AI_SYSTEM_OLLAMA, HttpClient, ModelsClient, NANOS_PER_MS, NANOS_PER_SECOND, NOOP_LOGGER, OllamaAbortError, OllamaAgentMaxIterationsError, OllamaAuthError, OllamaClient, OllamaClientError, OllamaGenericClientError, OllamaMcpError, OllamaNetworkError, OllamaNotFoundError, OllamaRateLimitError, OllamaServerError, OllamaSkillInvalidError, OllamaSkillNotFoundError, OllamaStream, OllamaTimeoutError, OllamaToolTimeoutError, OllamaToolValidationError, OllamaUnsupportedCapabilityError, OpenAICompatClient, ToolRegistry, addSpanEvent, applySkill, calculateBackoff, checkEndpointHealth, composeMiddleware, createConsoleLogger, createTimeoutSignal, defineTool, detectModelCapabilities, extractUsage, inferRuntimeMode, listAvailableModels, loadMcpTools, mapError, normalizeChatStream, normalizeGenerateStream, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseStructuredOutput, registerMcpTools, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
|
|
1968
|
+
//# sourceMappingURL=index.js.map
|
|
1969
|
+
//# sourceMappingURL=index.js.map
|