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