@nemesis-oss/ollama-sdk 1.1.0 → 1.4.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +43 -92
  2. package/README.md +749 -11
  3. package/dist/compose-BR70yb4W.d.cts +164 -0
  4. package/dist/compose-DfAqKAFj.d.ts +164 -0
  5. package/dist/generated-api.cjs +454 -0
  6. package/dist/generated-api.cjs.map +1 -0
  7. package/dist/generated-api.d.cts +1651 -0
  8. package/dist/generated-api.d.ts +1651 -0
  9. package/dist/generated-api.js +428 -0
  10. package/dist/generated-api.js.map +1 -0
  11. package/dist/generated-models-schemas.cjs +328 -0
  12. package/dist/generated-models-schemas.cjs.map +1 -0
  13. package/dist/generated-models-schemas.d.cts +463 -0
  14. package/dist/generated-models-schemas.d.ts +463 -0
  15. package/dist/generated-models-schemas.js +291 -0
  16. package/dist/generated-models-schemas.js.map +1 -0
  17. package/dist/generated-runtime.cjs +145 -0
  18. package/dist/generated-runtime.cjs.map +1 -0
  19. package/dist/generated-runtime.d.cts +2 -0
  20. package/dist/generated-runtime.d.ts +2 -0
  21. package/dist/generated-runtime.js +143 -0
  22. package/dist/generated-runtime.js.map +1 -0
  23. package/dist/index-2RIErNT5.d.cts +583 -0
  24. package/dist/index-DIQ8NUbU.d.ts +583 -0
  25. package/dist/index.cjs +2308 -184
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1123 -421
  28. package/dist/index.d.ts +1123 -421
  29. package/dist/index.js +2293 -185
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/tools.json +1058 -0
  32. package/dist/mcp-generated.cjs +386 -0
  33. package/dist/mcp-generated.cjs.map +1 -0
  34. package/dist/mcp-generated.d.cts +35 -0
  35. package/dist/mcp-generated.d.ts +35 -0
  36. package/dist/mcp-generated.js +381 -0
  37. package/dist/mcp-generated.js.map +1 -0
  38. package/dist/mcp-http.cjs +100 -0
  39. package/dist/mcp-http.cjs.map +1 -0
  40. package/dist/mcp-http.d.cts +48 -0
  41. package/dist/mcp-http.d.ts +48 -0
  42. package/dist/mcp-http.js +96 -0
  43. package/dist/mcp-http.js.map +1 -0
  44. package/dist/mcp-stdio.cjs +49 -0
  45. package/dist/mcp-stdio.cjs.map +1 -0
  46. package/dist/mcp-stdio.d.cts +34 -0
  47. package/dist/mcp-stdio.d.ts +34 -0
  48. package/dist/mcp-stdio.js +47 -0
  49. package/dist/mcp-stdio.js.map +1 -0
  50. package/dist/skills.cjs.map +1 -1
  51. package/dist/skills.d.cts +3 -2
  52. package/dist/skills.d.ts +3 -2
  53. package/dist/skills.js.map +1 -1
  54. package/dist/types-B9_axjum.d.cts +66 -0
  55. package/dist/types-B9_axjum.d.ts +66 -0
  56. package/dist/{compose-DJjVpDrk.d.cts → types-BQhgW-du.d.cts} +118 -172
  57. package/dist/{compose-DJjVpDrk.d.ts → types-BQhgW-du.d.ts} +118 -172
  58. package/package.json +96 -5
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  // src/config.ts
4
4
  var DEFAULT_BASE_URL = "http://localhost:11434";
5
5
  var DEFAULT_TIMEOUT_MS = 3e4;
6
+ var OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
6
7
  function readEnv(name) {
7
8
  return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
8
9
  }
@@ -12,6 +13,34 @@ function resolveBaseUrl(configBaseUrl) {
12
13
  function resolveApiKey(configApiKey) {
13
14
  return configApiKey ?? readEnv("OLLAMA_API_KEY");
14
15
  }
16
+ function resolveCredentialEndpoints(config) {
17
+ if (config.credentials === void 0) return [];
18
+ const modelsByCredential = /* @__PURE__ */ new Map();
19
+ for (const [model, bound] of Object.entries(config.modelBindings ?? {})) {
20
+ for (const id of typeof bound === "string" ? [bound] : bound) {
21
+ if (!(id in config.credentials)) {
22
+ throw new Error(
23
+ `modelBindings["${model}"] references unknown credential "${id}" \u2014 it isn't in \`credentials\`. Known credentials: ${Object.keys(config.credentials).join(", ") || "(none)"}.`
24
+ );
25
+ }
26
+ const list = modelsByCredential.get(id) ?? [];
27
+ list.push(model);
28
+ modelsByCredential.set(id, list);
29
+ }
30
+ }
31
+ return Object.entries(config.credentials).map(([id, credential]) => {
32
+ const isDefault = id === config.defaultCredential;
33
+ const boundModels = isDefault ? void 0 : modelsByCredential.get(id);
34
+ return {
35
+ name: `credential:${id}`,
36
+ baseUrl: resolveBaseUrl(credential.baseUrl ?? config.baseUrl),
37
+ apiKey: credential.apiKey,
38
+ ...credential.headers !== void 0 ? { headers: credential.headers } : {},
39
+ ...boundModels !== void 0 ? { models: boundModels } : {},
40
+ priority: isDefault ? -1 : 0
41
+ };
42
+ });
43
+ }
15
44
  var DEFAULT_FAILOVER_CODES = [
16
45
  "network_error",
17
46
  "timeout",
@@ -70,6 +99,15 @@ var OllamaRateLimitError = class extends OllamaClientError {
70
99
  this.retryAfterMs = options?.retryAfterMs;
71
100
  }
72
101
  };
102
+ var OllamaQuotaExceededError = class extends OllamaClientError {
103
+ windowId;
104
+ resetAt;
105
+ constructor(message, options) {
106
+ super(message, { ...options, code: "quota_exceeded", retryable: false });
107
+ this.windowId = options.windowId;
108
+ this.resetAt = options.resetAt;
109
+ }
110
+ };
73
111
  var OllamaServerError = class extends OllamaClientError {
74
112
  constructor(message, options) {
75
113
  super(message, { ...options, code: "server_error", retryable: true });
@@ -105,6 +143,35 @@ var OllamaUnsupportedCapabilityError = class extends OllamaClientError {
105
143
  this.capability = options.capability;
106
144
  }
107
145
  };
146
+ var OllamaModelRoutingError = class extends OllamaClientError {
147
+ model;
148
+ availableModels;
149
+ constructor(message, options) {
150
+ super(message, { ...options, code: "model_routing_error", retryable: false });
151
+ this.model = options.model;
152
+ this.availableModels = options.availableModels;
153
+ }
154
+ };
155
+ var OllamaIncompatibleModelError = class extends OllamaClientError {
156
+ model;
157
+ capability;
158
+ reportedCapabilities;
159
+ constructor(message, options) {
160
+ super(message, { ...options, code: "incompatible_model", retryable: false });
161
+ this.model = options.model;
162
+ this.capability = options.capability;
163
+ this.reportedCapabilities = options.reportedCapabilities;
164
+ }
165
+ };
166
+ var OllamaAgentMaxToolCallsError = class extends OllamaClientError {
167
+ maxToolCalls;
168
+ toolCallsExecuted;
169
+ constructor(message, options) {
170
+ super(message, { ...options, code: "agent_max_tool_calls_exceeded", retryable: false });
171
+ this.maxToolCalls = options.maxToolCalls;
172
+ this.toolCallsExecuted = options.toolCallsExecuted;
173
+ }
174
+ };
108
175
  var OllamaAgentMaxIterationsError = class extends OllamaClientError {
109
176
  maxIterations;
110
177
  constructor(message, options) {
@@ -216,17 +283,143 @@ var EndpointRegistry = class {
216
283
  failureThreshold;
217
284
  cooldownMs;
218
285
  now;
286
+ strategy;
287
+ roundRobinCounter = 0;
288
+ activeCounts = /* @__PURE__ */ new Map();
289
+ maxConcurrentPerEndpoint;
290
+ waiters = [];
219
291
  constructor(endpoints, options = {}) {
220
292
  this.endpoints = endpoints.length > 0 ? endpoints : [{ name: "default", baseUrl: "http://localhost:11434" }];
221
293
  this.failureThreshold = options.failureThreshold ?? 3;
222
294
  this.cooldownMs = options.cooldownMs ?? 3e4;
223
295
  this.now = options.now ?? Date.now;
296
+ this.strategy = options.strategy ?? "priority";
297
+ this.maxConcurrentPerEndpoint = options.maxConcurrentPerEndpoint;
298
+ }
299
+ /**
300
+ * Marks one more request as in-flight against `name`. Callers (`OllamaClient`) must
301
+ * pair every `acquire` with a `release` once that attempt finishes, success or not —
302
+ * see `OllamaClient.executeWithFailover`. Only affects candidate ordering when
303
+ * `strategy` is `'least-connections'`; harmless (just bookkeeping) otherwise, and
304
+ * always reflected in `status()`'s `activeRequests`.
305
+ */
306
+ acquire(name) {
307
+ this.activeCounts.set(name, (this.activeCounts.get(name) ?? 0) + 1);
308
+ }
309
+ /** Releases one in-flight request marked via `acquire`, waking one waiter for `name` if any. */
310
+ release(name) {
311
+ const next = (this.activeCounts.get(name) ?? 0) - 1;
312
+ if (next <= 0) {
313
+ this.activeCounts.delete(name);
314
+ } else {
315
+ this.activeCounts.set(name, next);
316
+ }
317
+ const idx = this.waiters.findIndex((w) => w.names.includes(name));
318
+ if (idx !== -1) {
319
+ const [waiter] = this.waiters.splice(idx, 1);
320
+ waiter?.cleanup();
321
+ waiter?.resolve();
322
+ }
323
+ }
324
+ /**
325
+ * `candidates` filtered down to those currently under `maxConcurrentPerEndpoint` (all
326
+ * of them, unchanged, if that option is unset).
327
+ */
328
+ filterWithCapacity(candidates) {
329
+ const cap = this.maxConcurrentPerEndpoint;
330
+ if (cap === void 0) return [...candidates];
331
+ return candidates.filter((ep) => (this.activeCounts.get(ep.name) ?? 0) < cap);
332
+ }
333
+ /**
334
+ * Resolves once any endpoint named in `names` has spare capacity under
335
+ * `maxConcurrentPerEndpoint` (a no-op resolve if that option is unset, or if one
336
+ * already does). Otherwise queues (FIFO per name) until a matching `release()` frees a
337
+ * slot, or rejects if `signal` aborts first — mirroring whatever aborted it
338
+ * (`OllamaTimeoutError`/the caller's own abort reason) so callers see the same error
339
+ * shape as any other in-flight request that times out or is cancelled. FIFO order is
340
+ * best-effort (a newly-arriving request can still win a race for a slot that just
341
+ * freed) — the capacity cap itself, not fairness, is the guarantee this exists for.
342
+ */
343
+ waitForCapacity(names, signal) {
344
+ const cap = this.maxConcurrentPerEndpoint;
345
+ if (cap === void 0) return Promise.resolve();
346
+ if (names.some((n) => (this.activeCounts.get(n) ?? 0) < cap)) return Promise.resolve();
347
+ return new Promise((resolve, reject) => {
348
+ const waiter = { names, resolve, cleanup: () => void 0 };
349
+ const onAbort = () => {
350
+ const idx = this.waiters.indexOf(waiter);
351
+ if (idx !== -1) this.waiters.splice(idx, 1);
352
+ reject(
353
+ signal?.reason instanceof Error ? signal.reason : new OllamaAbortError("Aborted while waiting for endpoint capacity")
354
+ );
355
+ };
356
+ if (signal !== void 0) {
357
+ if (signal.aborted) {
358
+ onAbort();
359
+ return;
360
+ }
361
+ signal.addEventListener("abort", onAbort, { once: true });
362
+ waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
363
+ }
364
+ this.waiters.push(waiter);
365
+ });
366
+ }
367
+ /**
368
+ * Rotates each same-`priority` group in `sorted` (already sorted highest-priority-first)
369
+ * by one position further per call, wrapping around within the group. Priority tiers
370
+ * themselves are never reordered — only candidates that were already tied.
371
+ */
372
+ applyRoundRobin(sorted) {
373
+ const offset = this.roundRobinCounter++;
374
+ const result = [];
375
+ let i = 0;
376
+ while (i < sorted.length) {
377
+ const priority = sorted[i]?.priority ?? 0;
378
+ let j = i + 1;
379
+ while (j < sorted.length && (sorted[j]?.priority ?? 0) === priority) j++;
380
+ const tier = sorted.slice(i, j);
381
+ const rotation = offset % tier.length;
382
+ result.push(...tier.slice(rotation), ...tier.slice(0, rotation));
383
+ i = j;
384
+ }
385
+ return result;
386
+ }
387
+ /**
388
+ * Reorders each same-`priority` group in `sorted` by ascending in-flight request count
389
+ * (from `acquire`/`release`); a stable sort, so candidates tied on active count keep
390
+ * their relative (priority-then-registration) order. Priority tiers themselves are
391
+ * never reordered.
392
+ */
393
+ applyLeastConnections(sorted) {
394
+ const result = [];
395
+ let i = 0;
396
+ while (i < sorted.length) {
397
+ const priority = sorted[i]?.priority ?? 0;
398
+ let j = i + 1;
399
+ while (j < sorted.length && (sorted[j]?.priority ?? 0) === priority) j++;
400
+ const tier = [...sorted.slice(i, j)].sort(
401
+ (a, b) => (this.activeCounts.get(a.name) ?? 0) - (this.activeCounts.get(b.name) ?? 0)
402
+ );
403
+ result.push(...tier);
404
+ i = j;
405
+ }
406
+ return result;
224
407
  }
225
- candidates() {
408
+ /**
409
+ * Endpoints eligible for `model`, sorted healthy-first-by-priority (falling back to
410
+ * soonest-to-recover if every eligible endpoint is cooling down); same-priority
411
+ * candidates are further reordered per `options.strategy` (see
412
+ * `EndpointRegistryOptions.strategy`). When `model` is given, endpoints whose `models`
413
+ * allow-list doesn't include it are excluded entirely — see `OllamaEndpoint.models`.
414
+ * Omit `model` to consider every configured endpoint, regardless of any `models`
415
+ * restriction (used for non-model-scoped operations).
416
+ */
417
+ candidates(model) {
226
418
  const currentTime = this.now();
419
+ const scoped = model === void 0 ? this.endpoints : this.endpoints.filter((ep) => ep.models === void 0 || ep.models.includes(model));
227
420
  const healthy = [];
228
421
  const coolingDown = [];
229
- for (const ep of this.endpoints) {
422
+ for (const ep of scoped) {
230
423
  const failures = this.failureCounts.get(ep.name) ?? 0;
231
424
  const lastFail = this.lastFailures.get(ep.name) ?? 0;
232
425
  const isCooling = failures >= this.failureThreshold && currentTime - lastFail < this.cooldownMs;
@@ -237,7 +430,10 @@ var EndpointRegistry = class {
237
430
  }
238
431
  }
239
432
  if (healthy.length > 0) {
240
- return healthy.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
433
+ const sorted = healthy.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
434
+ if (this.strategy === "round-robin") return this.applyRoundRobin(sorted);
435
+ if (this.strategy === "least-connections") return this.applyLeastConnections(sorted);
436
+ return sorted;
241
437
  }
242
438
  return coolingDown.sort((a, b) => a.recoveredAt - b.recoveredAt).map((c) => c.endpoint);
243
439
  }
@@ -253,6 +449,10 @@ var EndpointRegistry = class {
253
449
  list() {
254
450
  return this.endpoints;
255
451
  }
452
+ /** True if any configured endpoint restricts itself to a `models` allow-list. */
453
+ hasModelScopedEndpoints() {
454
+ return this.endpoints.some((ep) => ep.models !== void 0);
455
+ }
256
456
  status() {
257
457
  const currentTime = this.now();
258
458
  return this.endpoints.map((ep) => {
@@ -263,12 +463,35 @@ var EndpointRegistry = class {
263
463
  endpoint: ep,
264
464
  failureCount: failures,
265
465
  ...lastFail !== void 0 ? { lastFailureTimestamp: lastFail } : {},
266
- isCoolingDown
466
+ isCoolingDown,
467
+ activeRequests: this.activeCounts.get(ep.name) ?? 0
267
468
  };
268
469
  });
269
470
  }
270
471
  };
271
472
 
473
+ // src/middleware.ts
474
+ function composeMiddleware(middlewares, finalHandler) {
475
+ return function runPipeline(initialRequest) {
476
+ let index = -1;
477
+ function dispatch2(i, currentRequest) {
478
+ if (i <= index) {
479
+ return Promise.reject(new Error("next() called multiple times in middleware"));
480
+ }
481
+ index = i;
482
+ const fn = middlewares[i];
483
+ if (i === middlewares.length || !fn) {
484
+ return finalHandler(currentRequest);
485
+ }
486
+ return fn({
487
+ request: currentRequest,
488
+ next: () => dispatch2(i + 1, currentRequest)
489
+ });
490
+ }
491
+ return dispatch2(0, initialRequest);
492
+ };
493
+ }
494
+
272
495
  // src/streaming/ndjson.ts
273
496
  async function* parseNdjsonStream(stream) {
274
497
  const reader = stream.getReader();
@@ -297,6 +520,99 @@ async function* parseNdjsonStream(stream) {
297
520
  yield JSON.parse(buffer.trim());
298
521
  }
299
522
  } finally {
523
+ try {
524
+ await reader.cancel();
525
+ } catch {
526
+ }
527
+ reader.releaseLock();
528
+ }
529
+ }
530
+
531
+ // src/streaming/sse.ts
532
+ function dispatch(event, data, id, retry) {
533
+ if (data.length === 0) return void 0;
534
+ const joined = data.join("\n");
535
+ return {
536
+ ...event !== void 0 ? { event } : {},
537
+ data: joined.endsWith("\n") ? joined.slice(0, -1) : joined,
538
+ ...id !== void 0 ? { id } : {},
539
+ ...retry !== void 0 ? { retry } : {}
540
+ };
541
+ }
542
+ async function* parseSseStream(stream) {
543
+ const reader = stream.getReader();
544
+ const decoder = new TextDecoder();
545
+ let buffer = "";
546
+ let eventType;
547
+ let data = [];
548
+ let lastEventId;
549
+ let retry;
550
+ const processLine = (line) => {
551
+ if (line.length === 0) {
552
+ const event = dispatch(eventType, data, lastEventId, retry);
553
+ eventType = void 0;
554
+ data = [];
555
+ retry = void 0;
556
+ return event;
557
+ }
558
+ if (line.startsWith(":")) return void 0;
559
+ const separator = line.indexOf(":");
560
+ const field = separator === -1 ? line : line.slice(0, separator);
561
+ let value = separator === -1 ? "" : line.slice(separator + 1);
562
+ if (value.startsWith(" ")) value = value.slice(1);
563
+ switch (field) {
564
+ case "event":
565
+ eventType = value;
566
+ break;
567
+ case "data":
568
+ data.push(value);
569
+ break;
570
+ case "id":
571
+ if (!value.includes("\0")) lastEventId = value;
572
+ break;
573
+ case "retry": {
574
+ if (/^\d+$/.test(value)) {
575
+ const parsed = Number(value);
576
+ if (Number.isSafeInteger(parsed)) retry = parsed;
577
+ }
578
+ break;
579
+ }
580
+ }
581
+ return void 0;
582
+ };
583
+ try {
584
+ while (true) {
585
+ const { done, value } = await reader.read();
586
+ if (done) break;
587
+ buffer += decoder.decode(value, { stream: true });
588
+ while (true) {
589
+ const lf = buffer.indexOf("\n");
590
+ const cr = buffer.indexOf("\r");
591
+ if (lf === -1 && cr === -1) break;
592
+ if (cr !== -1 && lf === -1 && cr === buffer.length - 1) break;
593
+ const lineEnd = lf === -1 ? cr : cr === -1 ? lf : Math.min(lf, cr);
594
+ let separatorLength = 1;
595
+ if (buffer[lineEnd] === "\r" && buffer[lineEnd + 1] === "\n") {
596
+ separatorLength = 2;
597
+ }
598
+ const line = buffer.slice(0, lineEnd);
599
+ buffer = buffer.slice(lineEnd + separatorLength);
600
+ const event = processLine(line);
601
+ if (event) yield event;
602
+ }
603
+ }
604
+ buffer += decoder.decode();
605
+ if (buffer.length > 0) {
606
+ const event = processLine(buffer);
607
+ if (event) yield event;
608
+ }
609
+ const terminal = dispatch(eventType, data, lastEventId, retry);
610
+ if (terminal) yield terminal;
611
+ } finally {
612
+ try {
613
+ await reader.cancel();
614
+ } catch {
615
+ }
300
616
  reader.releaseLock();
301
617
  }
302
618
  }
@@ -381,16 +697,36 @@ function httpSpanAttributes(method, url) {
381
697
  [ATTR_SERVER_PORT]: target.port ? Number(target.port) : void 0
382
698
  };
383
699
  }
700
+ var requestSequence = 0;
701
+ function createRequestId() {
702
+ requestSequence += 1;
703
+ return `ollama-http-${requestSequence}`;
704
+ }
705
+ function sameHeaders(actual, expected) {
706
+ const actualEntries = Object.fromEntries(
707
+ Object.entries(actual).map(([key, value]) => [key.toLowerCase(), value])
708
+ );
709
+ const expectedEntries = Object.fromEntries(expected.entries());
710
+ const actualKeys = Object.keys(actualEntries);
711
+ const expectedKeys = Object.keys(expectedEntries);
712
+ return actualKeys.length === expectedKeys.length && expectedKeys.every((key) => actualEntries[key] === expectedEntries[key]);
713
+ }
384
714
  var HttpClient = class {
385
715
  baseUrl;
386
716
  apiKey;
387
717
  defaultHeaders;
388
718
  fetchImpl;
719
+ middleware;
720
+ onLifecycleEvent;
721
+ requestId;
389
722
  constructor(options) {
390
723
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
391
724
  this.apiKey = options.apiKey;
392
725
  this.defaultHeaders = options.headers ?? {};
393
726
  this.fetchImpl = options.fetch ?? globalThis.fetch;
727
+ this.middleware = options.middleware ?? [];
728
+ this.onLifecycleEvent = options.onLifecycleEvent;
729
+ this.requestId = options.requestId;
394
730
  }
395
731
  buildHeaders(customHeaders) {
396
732
  const headers = {
@@ -403,6 +739,131 @@ var HttpClient = class {
403
739
  }
404
740
  return headers;
405
741
  }
742
+ async fetchWithMiddleware(request) {
743
+ const requestId = this.requestId ?? createRequestId();
744
+ const startedAt = Date.now();
745
+ let rawResponse;
746
+ this.onLifecycleEvent?.({
747
+ type: "start",
748
+ requestId,
749
+ method: request.method,
750
+ url: request.url,
751
+ timestamp: startedAt
752
+ });
753
+ const finalHandler = async (req) => {
754
+ const init = {
755
+ method: req.method,
756
+ headers: req.headers,
757
+ ...req.body !== void 0 ? { body: req.body } : {},
758
+ ...req.signal !== void 0 ? { signal: req.signal } : {}
759
+ };
760
+ const response = await this.fetchImpl(req.url, init);
761
+ rawResponse = response;
762
+ const responseHeaders = response.headers && typeof response.headers.entries === "function" ? Object.fromEntries(response.headers.entries()) : {};
763
+ return {
764
+ status: response.status,
765
+ headers: responseHeaders,
766
+ body: response
767
+ };
768
+ };
769
+ try {
770
+ if (this.middleware.length === 0) {
771
+ const response2 = await this.fetchImpl(request.url, {
772
+ method: request.method,
773
+ headers: request.headers,
774
+ ...request.body !== void 0 ? { body: request.body } : {},
775
+ ...request.signal !== void 0 ? { signal: request.signal } : {}
776
+ });
777
+ if (response2.status >= 200 && response2.status < 300) {
778
+ this.onLifecycleEvent?.({
779
+ type: "success",
780
+ requestId,
781
+ durationMs: Date.now() - startedAt,
782
+ status: response2.status,
783
+ timestamp: Date.now()
784
+ });
785
+ } else {
786
+ this.onLifecycleEvent?.({
787
+ type: "error",
788
+ requestId,
789
+ durationMs: Date.now() - startedAt,
790
+ error: mapError(new Error(`HTTP ${response2.status}`), {
791
+ request: { method: request.method, url: request.url },
792
+ response: { status: response2.status }
793
+ }),
794
+ timestamp: Date.now()
795
+ });
796
+ }
797
+ return response2;
798
+ }
799
+ const pipeline = composeMiddleware(this.middleware, finalHandler);
800
+ const context = await pipeline(request);
801
+ let response;
802
+ if (rawResponse !== void 0 && context.body === rawResponse) {
803
+ response = rawResponse;
804
+ if (context.status !== response.status || !sameHeaders(context.headers, response.headers)) {
805
+ const cloned = response.clone();
806
+ response = new Response(cloned.body, {
807
+ status: context.status,
808
+ headers: context.headers
809
+ });
810
+ }
811
+ } else if (context.body instanceof Response) {
812
+ if (context.status === context.body.status && sameHeaders(context.headers, context.body.headers)) {
813
+ response = context.body;
814
+ } else {
815
+ const cloned = context.body.clone();
816
+ response = new Response(cloned.body, {
817
+ status: context.status,
818
+ headers: context.headers
819
+ });
820
+ }
821
+ } else if (context.body === void 0 || typeof context.body === "string" || context.body instanceof ArrayBuffer || ArrayBuffer.isView(context.body) || context.body instanceof Blob || context.body instanceof ReadableStream) {
822
+ response = new Response(context.body, {
823
+ status: context.status,
824
+ headers: context.headers
825
+ });
826
+ } else {
827
+ response = new Response(JSON.stringify(context.body), {
828
+ status: context.status,
829
+ headers: context.headers
830
+ });
831
+ }
832
+ if (context.status >= 200 && context.status < 300) {
833
+ this.onLifecycleEvent?.({
834
+ type: "success",
835
+ requestId,
836
+ durationMs: Date.now() - startedAt,
837
+ status: context.status,
838
+ timestamp: Date.now()
839
+ });
840
+ } else {
841
+ this.onLifecycleEvent?.({
842
+ type: "error",
843
+ requestId,
844
+ durationMs: Date.now() - startedAt,
845
+ error: mapError(new Error(`HTTP ${context.status}`), {
846
+ request: { method: request.method, url: request.url },
847
+ response: { status: context.status, headers: context.headers }
848
+ }),
849
+ timestamp: Date.now()
850
+ });
851
+ }
852
+ return response;
853
+ } catch (error) {
854
+ const mapped = mapError(error, {
855
+ request: { method: request.method, url: request.url }
856
+ });
857
+ this.onLifecycleEvent?.({
858
+ type: "error",
859
+ requestId,
860
+ durationMs: Date.now() - startedAt,
861
+ error: mapped,
862
+ timestamp: Date.now()
863
+ });
864
+ throw mapped;
865
+ }
866
+ }
406
867
  async request(options) {
407
868
  const url = `${this.baseUrl}${options.path}`;
408
869
  const method = options.method ?? (options.body !== void 0 || options.rawBody !== void 0 ? "POST" : "GET");
@@ -416,13 +877,13 @@ var HttpClient = class {
416
877
  async (span) => {
417
878
  try {
418
879
  const bodyInit = options.rawBody !== void 0 ? options.rawBody : options.body !== void 0 ? JSON.stringify(options.body) : void 0;
419
- const init = {
880
+ const response = await this.fetchWithMiddleware({
881
+ url,
420
882
  method,
421
883
  headers,
422
884
  ...bodyInit !== void 0 ? { body: bodyInit } : {},
423
885
  ...options.signal !== void 0 ? { signal: options.signal } : {}
424
- };
425
- const response = await this.fetchImpl(url, init);
886
+ });
426
887
  span?.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
427
888
  if (!response.ok) {
428
889
  let errorBody;
@@ -440,6 +901,9 @@ var HttpClient = class {
440
901
  if (options.method === "HEAD" || response.status === 204) {
441
902
  return void 0;
442
903
  }
904
+ if (options.raw === true) {
905
+ return response;
906
+ }
443
907
  if (typeof response.text === "function") {
444
908
  const text = await response.text();
445
909
  return text ? JSON.parse(text) : void 0;
@@ -454,6 +918,79 @@ var HttpClient = class {
454
918
  }
455
919
  );
456
920
  }
921
+ /**
922
+ * Opens an event-stream response and exposes parsed SSE events.
923
+ *
924
+ * This is intentionally schema-agnostic so OpenAI and Anthropic compatibility layers
925
+ * can decode their provider-specific event payloads without duplicating transport logic.
926
+ */
927
+ async requestSseStream(options) {
928
+ const url = `${this.baseUrl}${options.path}`;
929
+ const method = options.method ?? "POST";
930
+ const headers = {
931
+ ...this.buildHeaders(options.headers),
932
+ Accept: "text/event-stream"
933
+ };
934
+ return withSpan(
935
+ `${method} ${routeTemplate(options.path)}`,
936
+ httpSpanAttributes(method, url),
937
+ async (span) => {
938
+ try {
939
+ const controller = new AbortController();
940
+ const removeAbortListener = options.signal !== void 0 ? (() => {
941
+ if (options.signal.aborted) {
942
+ controller.abort(options.signal.reason);
943
+ } else {
944
+ const onAbort = () => controller.abort(options.signal?.reason);
945
+ options.signal.addEventListener("abort", onAbort, { once: true });
946
+ return () => options.signal?.removeEventListener("abort", onAbort);
947
+ }
948
+ return void 0;
949
+ })() : void 0;
950
+ const response = await this.fetchWithMiddleware({
951
+ url,
952
+ method,
953
+ headers,
954
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
955
+ signal: controller.signal
956
+ });
957
+ span?.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
958
+ if (!response.ok) {
959
+ removeAbortListener?.();
960
+ const errorText = await response.text();
961
+ throw mapError(new Error(errorText || `HTTP ${response.status}`), {
962
+ request: { method, url },
963
+ response: { status: response.status, body: errorText }
964
+ });
965
+ }
966
+ if (!response.body) {
967
+ removeAbortListener?.();
968
+ throw mapError(new Error("Response body is null, cannot stream SSE"), {
969
+ request: { method, url }
970
+ });
971
+ }
972
+ const stream = (async function* () {
973
+ try {
974
+ yield* parseSseStream(response.body);
975
+ } finally {
976
+ removeAbortListener?.();
977
+ }
978
+ })();
979
+ return {
980
+ [Symbol.asyncIterator]() {
981
+ return stream[Symbol.asyncIterator]();
982
+ },
983
+ abort: () => {
984
+ removeAbortListener?.();
985
+ controller.abort();
986
+ }
987
+ };
988
+ } catch (err) {
989
+ throw mapError(err, { request: { method, url } });
990
+ }
991
+ }
992
+ );
993
+ }
457
994
  async requestStream(options) {
458
995
  const url = `${this.baseUrl}${options.path}`;
459
996
  const method = options.method ?? "POST";
@@ -463,15 +1000,27 @@ var HttpClient = class {
463
1000
  httpSpanAttributes(method, url),
464
1001
  async (span) => {
465
1002
  try {
466
- const init = {
1003
+ const controller = new AbortController();
1004
+ const removeAbortListener = options.signal !== void 0 ? (() => {
1005
+ if (options.signal.aborted) {
1006
+ controller.abort(options.signal.reason);
1007
+ } else {
1008
+ const onAbort = () => controller.abort(options.signal?.reason);
1009
+ options.signal.addEventListener("abort", onAbort, { once: true });
1010
+ return () => options.signal?.removeEventListener("abort", onAbort);
1011
+ }
1012
+ return void 0;
1013
+ })() : void 0;
1014
+ const response = await this.fetchWithMiddleware({
1015
+ url,
467
1016
  method,
468
1017
  headers,
469
1018
  ...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);
1019
+ signal: controller.signal
1020
+ });
473
1021
  span?.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
474
1022
  if (!response.ok) {
1023
+ removeAbortListener?.();
475
1024
  const errorText = await response.text();
476
1025
  throw mapError(new Error(errorText || `HTTP ${response.status}`), {
477
1026
  request: { method, url },
@@ -479,6 +1028,7 @@ var HttpClient = class {
479
1028
  });
480
1029
  }
481
1030
  if (!response.body) {
1031
+ removeAbortListener?.();
482
1032
  throw mapError(new Error("Response body is null, cannot stream"), {
483
1033
  request: { method, url }
484
1034
  });
@@ -486,7 +1036,17 @@ var HttpClient = class {
486
1036
  const stream = parseNdjsonStream(response.body);
487
1037
  const abortable = {
488
1038
  [Symbol.asyncIterator]() {
489
- return stream[Symbol.asyncIterator]();
1039
+ return (async function* () {
1040
+ try {
1041
+ yield* stream;
1042
+ } finally {
1043
+ removeAbortListener?.();
1044
+ }
1045
+ })()[Symbol.asyncIterator]();
1046
+ },
1047
+ abort: () => {
1048
+ removeAbortListener?.();
1049
+ controller.abort();
490
1050
  }
491
1051
  };
492
1052
  return abortable;
@@ -499,12 +1059,14 @@ var HttpClient = class {
499
1059
  };
500
1060
 
501
1061
  // src/providers/health-check.ts
502
- async function checkEndpointHealth(endpoint, fetchImpl, timeoutMs = 5e3) {
1062
+ async function checkEndpointHealth(endpoint, fetchImpl, timeoutMs = 5e3, middleware, onLifecycleEvent) {
503
1063
  const client = new HttpClient({
504
1064
  baseUrl: endpoint.baseUrl,
505
1065
  apiKey: endpoint.apiKey,
506
1066
  headers: endpoint.headers,
507
- fetch: fetchImpl
1067
+ fetch: fetchImpl,
1068
+ middleware,
1069
+ onLifecycleEvent
508
1070
  });
509
1071
  const startTime = Date.now();
510
1072
  const controller = new AbortController();
@@ -550,13 +1112,24 @@ function inferRuntimeMode(baseUrl) {
550
1112
  return "unknown";
551
1113
  }
552
1114
  }
553
- async function detectModelCapabilities(http, model) {
1115
+ function extractContextLength(modelInfo) {
1116
+ if (!modelInfo) return void 0;
1117
+ for (const [key, value] of Object.entries(modelInfo)) {
1118
+ if (key.endsWith(".context_length") && typeof value === "number" && Number.isFinite(value) && value > 0) {
1119
+ return value;
1120
+ }
1121
+ }
1122
+ return void 0;
1123
+ }
1124
+ async function detectModelCapabilities(http, model, signal) {
554
1125
  const showRes = await http.request({
555
1126
  path: "/api/show",
556
- body: { model }
1127
+ body: { model },
1128
+ ...signal !== void 0 ? { signal } : {}
557
1129
  });
558
1130
  const reported = showRes.capabilities ?? [];
559
1131
  const reportedSet = new Set(reported.map((c) => c.toLowerCase()));
1132
+ const contextLength = extractContextLength(showRes.model_info);
560
1133
  return {
561
1134
  model,
562
1135
  reported,
@@ -565,6 +1138,8 @@ async function detectModelCapabilities(http, model) {
565
1138
  supportsEmbedding: reportedSet.has("embedding"),
566
1139
  supportsCompletion: reportedSet.has("completion") || !reportedSet.has("embedding"),
567
1140
  supportsThinking: reportedSet.has("thinking"),
1141
+ ...showRes.thinking !== void 0 ? { thinking: showRes.thinking } : {},
1142
+ ...contextLength !== void 0 ? { contextLength } : {},
568
1143
  supportsStreaming: true,
569
1144
  supportsStructuredOutputRequest: inferRuntimeMode(http.baseUrl) !== "cloud"
570
1145
  };
@@ -689,6 +1264,7 @@ function extractUsage(raw) {
689
1264
  }
690
1265
  return {
691
1266
  promptTokens,
1267
+ ...raw.prompt_eval_cached_count !== void 0 ? { cachedPromptTokens: raw.prompt_eval_cached_count } : {},
692
1268
  completionTokens,
693
1269
  totalTokens,
694
1270
  ...tokensPerSecond !== void 0 ? { tokensPerSecond } : {},
@@ -712,7 +1288,7 @@ function ensureToolCallIds(toolCalls) {
712
1288
 
713
1289
  // src/streaming/stream.ts
714
1290
  var OllamaStream = class {
715
- constructor(source, mapChunk, aggregate, initial) {
1291
+ constructor(source, mapChunk, aggregate, initial, signal) {
716
1292
  this.source = source;
717
1293
  this.mapChunk = mapChunk;
718
1294
  this.aggregate = aggregate;
@@ -721,6 +1297,15 @@ var OllamaStream = class {
721
1297
  this.resolveFinal = resolve;
722
1298
  this.rejectFinal = reject;
723
1299
  });
1300
+ if (signal !== void 0) {
1301
+ const onAbort = () => this.abort();
1302
+ if (signal.aborted) {
1303
+ onAbort();
1304
+ } else {
1305
+ signal.addEventListener("abort", onAbort, { once: true });
1306
+ this.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
1307
+ }
1308
+ }
724
1309
  }
725
1310
  source;
726
1311
  mapChunk;
@@ -731,11 +1316,15 @@ var OllamaStream = class {
731
1316
  finalResultPromise;
732
1317
  resolveFinal;
733
1318
  rejectFinal;
1319
+ removeAbortListener;
734
1320
  get finalResult() {
735
1321
  return this.finalResultPromise;
736
1322
  }
737
1323
  abort() {
1324
+ this.removeAbortListener?.();
1325
+ this.removeAbortListener = void 0;
738
1326
  this.source.abort?.();
1327
+ this.rejectFinal(new OllamaAbortError("Ollama stream aborted"));
739
1328
  }
740
1329
  on(type, listener) {
741
1330
  if (this.mode === "iterator") {
@@ -775,7 +1364,11 @@ var OllamaStream = class {
775
1364
  }
776
1365
  }
777
1366
  }
1367
+ this.removeAbortListener?.();
1368
+ this.removeAbortListener = void 0;
778
1369
  } catch (error) {
1370
+ this.removeAbortListener?.();
1371
+ this.removeAbortListener = void 0;
779
1372
  const mapped = mapError(error);
780
1373
  this.emit({ type: "error", data: { error: mapped } });
781
1374
  this.rejectFinal(mapped);
@@ -789,12 +1382,14 @@ var OllamaStream = class {
789
1382
  }
790
1383
  this.mode = "iterator";
791
1384
  let accumulated = this.initial;
1385
+ let completed = false;
792
1386
  try {
793
1387
  for await (const chunk of this.source) {
794
1388
  accumulated = this.aggregate(accumulated, chunk);
795
1389
  const events = this.mapChunk(chunk, accumulated);
796
1390
  for (const event of events) {
797
1391
  if (event.type === "done") {
1392
+ completed = true;
798
1393
  this.resolveFinal(event.data.result);
799
1394
  }
800
1395
  yield event;
@@ -804,6 +1399,13 @@ var OllamaStream = class {
804
1399
  const mapped = mapError(error);
805
1400
  this.rejectFinal(mapped);
806
1401
  yield { type: "error", data: { error: mapped } };
1402
+ } finally {
1403
+ this.removeAbortListener?.();
1404
+ this.removeAbortListener = void 0;
1405
+ if (!completed) {
1406
+ this.source.abort?.();
1407
+ this.rejectFinal(new OllamaAbortError("Ollama stream ended before completion"));
1408
+ }
807
1409
  }
808
1410
  }
809
1411
  };
@@ -857,13 +1459,13 @@ function withStableToolCallIds(source) {
857
1459
  }
858
1460
  return Object.assign(generator(), { abort: source.abort });
859
1461
  }
860
- function normalizeChatStream(source) {
1462
+ function normalizeChatStream(source, signal) {
861
1463
  const initial = {
862
1464
  message: { role: "assistant", content: "" },
863
1465
  model: "",
864
1466
  done: false
865
1467
  };
866
- return new OllamaStream(withStableToolCallIds(source), mapChatChunk, aggregateChat, initial);
1468
+ return new OllamaStream(withStableToolCallIds(source), mapChatChunk, aggregateChat, initial, signal);
867
1469
  }
868
1470
  function aggregateGenerate(accumulated, chunk) {
869
1471
  return {
@@ -873,6 +1475,10 @@ function aggregateGenerate(accumulated, chunk) {
873
1475
  doneReason: chunk.done ? chunk.done_reason : accumulated.doneReason,
874
1476
  totalDurationMs: chunk.done && chunk.total_duration !== void 0 ? chunk.total_duration / NANOS_PER_MS : accumulated.totalDurationMs,
875
1477
  usage: chunk.done ? extractUsage(chunk) : accumulated.usage,
1478
+ thinking: chunk.thinking !== void 0 ? (accumulated.thinking ?? "") + chunk.thinking : accumulated.thinking,
1479
+ image: chunk.image ?? accumulated.image,
1480
+ completed: chunk.completed ?? accumulated.completed,
1481
+ total: chunk.total ?? accumulated.total,
876
1482
  raw: chunk
877
1483
  };
878
1484
  }
@@ -890,9 +1496,9 @@ function mapGenerateChunk(chunk, aggregated) {
890
1496
  }
891
1497
  return events;
892
1498
  }
893
- function normalizeGenerateStream(source) {
1499
+ function normalizeGenerateStream(source, signal) {
894
1500
  const initial = { response: "", model: "", done: false };
895
- return new OllamaStream(source, mapGenerateChunk, aggregateGenerate, initial);
1501
+ return new OllamaStream(source, mapGenerateChunk, aggregateGenerate, initial, signal);
896
1502
  }
897
1503
  function aggregateProgress(_accumulated, chunk) {
898
1504
  return {
@@ -912,9 +1518,9 @@ function mapProgressChunk(chunk, aggregated) {
912
1518
  }
913
1519
  return events;
914
1520
  }
915
- function normalizeProgressStream(source) {
1521
+ function normalizeProgressStream(source, signal) {
916
1522
  const initial = { status: "", done: false };
917
- return new OllamaStream(source, mapProgressChunk, aggregateProgress, initial);
1523
+ return new OllamaStream(source, mapProgressChunk, aggregateProgress, initial, signal);
918
1524
  }
919
1525
 
920
1526
  // src/transport/backoff.ts
@@ -940,8 +1546,33 @@ function isRetryableDefault(error) {
940
1546
  }
941
1547
  return false;
942
1548
  }
943
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
944
- async function withRetry(operation, config = DEFAULT_RETRY_CONFIG) {
1549
+ function abortError(signal, fallbackMessage) {
1550
+ const reason = signal?.reason;
1551
+ if (reason instanceof OllamaClientError) return reason;
1552
+ return new OllamaAbortError(
1553
+ reason instanceof Error && reason.message ? reason.message : fallbackMessage,
1554
+ { cause: reason }
1555
+ );
1556
+ }
1557
+ function sleep(ms, signal) {
1558
+ return new Promise((resolve, reject) => {
1559
+ if (signal?.aborted) {
1560
+ reject(abortError(signal, "Retry backoff aborted"));
1561
+ return;
1562
+ }
1563
+ const timerId = setTimeout(() => {
1564
+ signal?.removeEventListener("abort", onAbort);
1565
+ resolve();
1566
+ }, ms);
1567
+ const onAbort = () => {
1568
+ if (timerId !== void 0) clearTimeout(timerId);
1569
+ signal?.removeEventListener("abort", onAbort);
1570
+ reject(abortError(signal, "Retry backoff aborted"));
1571
+ };
1572
+ signal?.addEventListener("abort", onAbort, { once: true });
1573
+ });
1574
+ }
1575
+ async function withRetry(operation, config = DEFAULT_RETRY_CONFIG, signal) {
945
1576
  let attempt = 0;
946
1577
  while (true) {
947
1578
  try {
@@ -954,7 +1585,7 @@ async function withRetry(operation, config = DEFAULT_RETRY_CONFIG) {
954
1585
  }
955
1586
  const delayMs = calculateBackoff(attempt, config.backoff);
956
1587
  config.onRetry?.(error, attempt, delayMs);
957
- await sleep(delayMs);
1588
+ await sleep(delayMs, signal);
958
1589
  attempt += 1;
959
1590
  }
960
1591
  }
@@ -1020,9 +1651,9 @@ var ModelsClient = class {
1020
1651
  body: { ...request, stream: true },
1021
1652
  signal
1022
1653
  });
1023
- return normalizeProgressStream(stream);
1654
+ return normalizeProgressStream(stream, signal);
1024
1655
  },
1025
- { ...request, singleEndpoint: true }
1656
+ { ...request, singleEndpoint: true, holdUntil: (stream) => stream.finalResult }
1026
1657
  );
1027
1658
  }
1028
1659
  return this.runner(
@@ -1043,9 +1674,9 @@ var ModelsClient = class {
1043
1674
  body: { ...request, stream: true },
1044
1675
  signal
1045
1676
  });
1046
- return normalizeProgressStream(stream);
1677
+ return normalizeProgressStream(stream, signal);
1047
1678
  },
1048
- { ...request, singleEndpoint: true }
1679
+ { ...request, singleEndpoint: true, holdUntil: (stream) => stream.finalResult }
1049
1680
  );
1050
1681
  }
1051
1682
  return this.runner(
@@ -1068,7 +1699,7 @@ var ModelsClient = class {
1068
1699
  });
1069
1700
  return normalizeProgressStream(stream);
1070
1701
  },
1071
- { ...request, singleEndpoint: true }
1702
+ { ...request, singleEndpoint: true, holdUntil: (stream) => stream.finalResult }
1072
1703
  );
1073
1704
  }
1074
1705
  return this.runner(
@@ -1137,58 +1768,944 @@ var ModelsClient = class {
1137
1768
  };
1138
1769
 
1139
1770
  // src/integrations/openai.ts
1140
- var OpenAICompatClient = class {
1141
- constructor(http) {
1142
- this.http = http;
1771
+ var OpenAIResponsesStreamError = class extends OllamaClientError {
1772
+ responsePayload;
1773
+ constructor(message, responsePayload) {
1774
+ super(message, { code: "openai_responses_stream_error", retryable: false, response: { body: responsePayload } });
1775
+ this.responsePayload = responsePayload;
1143
1776
  }
1144
- http;
1145
- async createChatCompletion(request, signal) {
1146
- return this.http.request({
1147
- path: "/v1/chat/completions",
1148
- body: request,
1149
- signal
1777
+ };
1778
+ var OpenAIChatCompletionStream = class {
1779
+ constructor(source, signal) {
1780
+ this.source = source;
1781
+ this.finalResultPromise = new Promise((resolve, reject) => {
1782
+ this.resolveFinal = resolve;
1783
+ this.rejectFinal = reject;
1150
1784
  });
1785
+ if (signal !== void 0) {
1786
+ const onAbort = () => this.abort();
1787
+ if (signal.aborted) onAbort();
1788
+ else {
1789
+ signal.addEventListener("abort", onAbort, { once: true });
1790
+ this.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
1791
+ }
1792
+ }
1151
1793
  }
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
- });
1794
+ source;
1795
+ finalResultPromise;
1796
+ resolveFinal;
1797
+ rejectFinal;
1798
+ removeAbortListener;
1799
+ get finalResult() {
1800
+ return this.finalResultPromise;
1161
1801
  }
1162
- async createResponses(request, signal) {
1163
- return this.http.request({
1164
- path: "/v1/responses",
1165
- body: request,
1166
- signal
1167
- });
1802
+ abort() {
1803
+ this.removeAbortListener?.();
1804
+ this.removeAbortListener = void 0;
1805
+ this.source.abort?.();
1806
+ this.rejectFinal(new OllamaAbortError("OpenAI Chat Completion stream aborted"));
1168
1807
  }
1169
- async responses(request, signal) {
1170
- return this.createResponses(request, signal);
1808
+ async *[Symbol.asyncIterator]() {
1809
+ const choices = /* @__PURE__ */ new Map();
1810
+ let id = "";
1811
+ let model = "";
1812
+ let created = 0;
1813
+ let systemFingerprint;
1814
+ let usage;
1815
+ let completed = false;
1816
+ try {
1817
+ for await (const event of this.source) {
1818
+ if (event.data === "[DONE]") break;
1819
+ let chunk;
1820
+ try {
1821
+ chunk = JSON.parse(event.data);
1822
+ } catch (error) {
1823
+ throw new Error("Failed to parse OpenAI chat completion SSE payload", { cause: error });
1824
+ }
1825
+ id = chunk.id || id;
1826
+ model = chunk.model || model;
1827
+ created = chunk.created || created;
1828
+ if (chunk.system_fingerprint !== void 0) systemFingerprint = chunk.system_fingerprint;
1829
+ if (chunk.usage !== null && chunk.usage !== void 0) usage = chunk.usage;
1830
+ for (const choice of chunk.choices) {
1831
+ const existing = choices.get(choice.index);
1832
+ const delta = choice.delta;
1833
+ if (!existing) {
1834
+ const toolCalls = delta.tool_calls?.map((toolCall) => ({
1835
+ id: toolCall.id ?? "",
1836
+ type: "function",
1837
+ function: {
1838
+ name: toolCall.function?.name ?? "",
1839
+ arguments: toolCall.function?.arguments ?? ""
1840
+ }
1841
+ }));
1842
+ choices.set(choice.index, {
1843
+ message: {
1844
+ role: delta.role ?? "assistant",
1845
+ content: delta.content ?? "",
1846
+ ...delta.reasoning !== void 0 && delta.reasoning !== null ? { reasoning: delta.reasoning } : {},
1847
+ ...toolCalls?.length ? { tool_calls: toolCalls } : {}
1848
+ },
1849
+ finish_reason: choice.finish_reason ?? "",
1850
+ ...choice.logprobs !== void 0 ? { logprobs: choice.logprobs } : {}
1851
+ });
1852
+ continue;
1853
+ }
1854
+ const priorToolCalls = [...existing.message.tool_calls ?? []];
1855
+ for (const toolCall of delta.tool_calls ?? []) {
1856
+ const position = toolCall.index;
1857
+ const current = priorToolCalls[position];
1858
+ if (current) {
1859
+ priorToolCalls[position] = {
1860
+ ...current,
1861
+ ...toolCall.id !== void 0 ? { id: toolCall.id } : {},
1862
+ function: {
1863
+ name: current.function?.name ?? toolCall.function?.name ?? "",
1864
+ arguments: (current.function?.arguments ?? "") + (toolCall.function?.arguments ?? "")
1865
+ }
1866
+ };
1867
+ } else {
1868
+ priorToolCalls[position] = {
1869
+ id: toolCall.id ?? "",
1870
+ type: "function",
1871
+ function: {
1872
+ name: toolCall.function?.name ?? "",
1873
+ arguments: toolCall.function?.arguments ?? ""
1874
+ }
1875
+ };
1876
+ }
1877
+ }
1878
+ choices.set(choice.index, {
1879
+ message: {
1880
+ ...existing.message,
1881
+ ...delta.content !== void 0 && delta.content !== null ? { content: existing.message.content + delta.content } : {},
1882
+ ...delta.reasoning !== void 0 && delta.reasoning !== null ? { reasoning: (existing.message.reasoning ?? "") + delta.reasoning } : {},
1883
+ ...priorToolCalls.length ? { tool_calls: priorToolCalls } : {}
1884
+ },
1885
+ finish_reason: choice.finish_reason ?? existing.finish_reason,
1886
+ ...choice.logprobs !== void 0 ? { logprobs: choice.logprobs } : {}
1887
+ });
1888
+ }
1889
+ yield chunk;
1890
+ }
1891
+ const response = {
1892
+ id,
1893
+ object: "chat.completion",
1894
+ created,
1895
+ model,
1896
+ ...systemFingerprint !== void 0 ? { system_fingerprint: systemFingerprint } : {},
1897
+ choices: [...choices.entries()].sort(([a], [b]) => a - b).map(([index, choice]) => ({
1898
+ index,
1899
+ message: choice.message,
1900
+ finish_reason: choice.finish_reason,
1901
+ ...choice.logprobs !== void 0 ? { logprobs: choice.logprobs } : {}
1902
+ })),
1903
+ ...usage !== void 0 ? { usage } : {}
1904
+ };
1905
+ completed = true;
1906
+ this.removeAbortListener?.();
1907
+ this.removeAbortListener = void 0;
1908
+ this.resolveFinal(response);
1909
+ } catch (error) {
1910
+ this.removeAbortListener?.();
1911
+ this.removeAbortListener = void 0;
1912
+ this.rejectFinal(error);
1913
+ throw error;
1914
+ } finally {
1915
+ if (!completed) {
1916
+ this.source.abort?.();
1917
+ this.rejectFinal(new OllamaAbortError("OpenAI Chat Completion stream ended before completion"));
1918
+ }
1919
+ }
1171
1920
  }
1172
1921
  };
1173
-
1174
- // src/integrations/anthropic.ts
1175
- var AnthropicCompatClient = class {
1176
- constructor(http) {
1177
- this.http = http;
1178
- }
1179
- http;
1180
- async createMessage(request, signal) {
1181
- return this.http.request({
1182
- path: "/v1/messages",
1183
- body: request,
1184
- signal
1922
+ var OpenAICompletionStream = class {
1923
+ constructor(source, signal) {
1924
+ this.source = source;
1925
+ this.finalResultPromise = new Promise((resolve, reject) => {
1926
+ this.resolveFinal = resolve;
1927
+ this.rejectFinal = reject;
1185
1928
  });
1929
+ if (signal !== void 0) {
1930
+ const onAbort = () => this.abort();
1931
+ if (signal.aborted) onAbort();
1932
+ else {
1933
+ signal.addEventListener("abort", onAbort, { once: true });
1934
+ this.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
1935
+ }
1936
+ }
1937
+ }
1938
+ source;
1939
+ finalResultPromise;
1940
+ resolveFinal;
1941
+ rejectFinal;
1942
+ removeAbortListener;
1943
+ get finalResult() {
1944
+ return this.finalResultPromise;
1945
+ }
1946
+ abort() {
1947
+ this.removeAbortListener?.();
1948
+ this.removeAbortListener = void 0;
1949
+ this.source.abort?.();
1950
+ this.rejectFinal(new OllamaAbortError("OpenAI Completion stream aborted"));
1951
+ }
1952
+ async *[Symbol.asyncIterator]() {
1953
+ const texts = /* @__PURE__ */ new Map();
1954
+ let id = "";
1955
+ let model = "";
1956
+ let created = 0;
1957
+ let usage;
1958
+ let completed = false;
1959
+ try {
1960
+ for await (const event of this.source) {
1961
+ if (event.data === "[DONE]") break;
1962
+ const chunk = JSON.parse(event.data);
1963
+ id = chunk.id || id;
1964
+ model = chunk.model || model;
1965
+ created = chunk.created || created;
1966
+ if (chunk.usage !== null && chunk.usage !== void 0) usage = chunk.usage;
1967
+ for (const choice of chunk.choices) {
1968
+ const existing = texts.get(choice.index);
1969
+ texts.set(choice.index, {
1970
+ text: (existing?.text ?? "") + (choice.text ?? ""),
1971
+ index: choice.index,
1972
+ finish_reason: choice.finish_reason ?? existing?.finish_reason ?? null,
1973
+ ...choice.logprobs !== void 0 ? { logprobs: choice.logprobs } : {}
1974
+ });
1975
+ }
1976
+ yield chunk;
1977
+ }
1978
+ const response = {
1979
+ id,
1980
+ object: "text_completion",
1981
+ created,
1982
+ model,
1983
+ choices: [...texts.values()].sort((a, b) => a.index - b.index),
1984
+ ...usage !== void 0 ? { usage } : {}
1985
+ };
1986
+ completed = true;
1987
+ this.removeAbortListener?.();
1988
+ this.removeAbortListener = void 0;
1989
+ this.resolveFinal(response);
1990
+ } catch (error) {
1991
+ this.removeAbortListener?.();
1992
+ this.removeAbortListener = void 0;
1993
+ this.rejectFinal(error);
1994
+ throw error;
1995
+ } finally {
1996
+ if (!completed) {
1997
+ this.source.abort?.();
1998
+ this.rejectFinal(new OllamaAbortError("OpenAI Completion stream ended before completion"));
1999
+ }
2000
+ }
2001
+ }
2002
+ };
2003
+ var OpenAIResponsesStream = class {
2004
+ constructor(source, signal) {
2005
+ this.source = source;
2006
+ this.finalResultPromise = new Promise((resolve, reject) => {
2007
+ this.resolveFinal = resolve;
2008
+ this.rejectFinal = reject;
2009
+ });
2010
+ if (signal !== void 0) {
2011
+ const onAbort = () => this.abort();
2012
+ if (signal.aborted) onAbort();
2013
+ else {
2014
+ signal.addEventListener("abort", onAbort, { once: true });
2015
+ this.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
2016
+ }
2017
+ }
2018
+ }
2019
+ source;
2020
+ finalResultPromise;
2021
+ resolveFinal;
2022
+ rejectFinal;
2023
+ removeAbortListener;
2024
+ get finalResult() {
2025
+ return this.finalResultPromise;
2026
+ }
2027
+ abort() {
2028
+ this.removeAbortListener?.();
2029
+ this.removeAbortListener = void 0;
2030
+ this.source.abort?.();
2031
+ this.rejectFinal(new OllamaAbortError("OpenAI Responses stream aborted"));
2032
+ }
2033
+ async *[Symbol.asyncIterator]() {
2034
+ let finalResponse;
2035
+ let responseMeta;
2036
+ let terminalFailure;
2037
+ let terminalIncomplete;
2038
+ const outputs = /* @__PURE__ */ new Map();
2039
+ const canonicalOutputItems = /* @__PURE__ */ new Map();
2040
+ let completed = false;
2041
+ const ensureState = (outputIndex, itemId, kind) => {
2042
+ const existing = outputs.get(outputIndex);
2043
+ if (existing) {
2044
+ if (itemId) existing.itemId = itemId;
2045
+ existing.kind = kind;
2046
+ return existing;
2047
+ }
2048
+ const state = { itemId, kind, content: /* @__PURE__ */ new Map(), summary: /* @__PURE__ */ new Map(), arguments: "", reasoningText: "" };
2049
+ outputs.set(outputIndex, state);
2050
+ return state;
2051
+ };
2052
+ const applyOutputItem = (outputIndex, item) => {
2053
+ const kind = item.type === "message" ? "message" : item.type === "function_call" ? "function_call" : "reasoning";
2054
+ const state = ensureState(outputIndex, item.id ?? "", kind);
2055
+ state.itemId = item.id ?? state.itemId;
2056
+ state.status = item.status;
2057
+ if (item.type === "message") {
2058
+ state.role = item.role;
2059
+ state.phase = item.phase;
2060
+ state.content.clear();
2061
+ item.content.forEach((p, i) => state.content.set(i, p));
2062
+ } else if (item.type === "function_call") {
2063
+ state.callId = item.call_id;
2064
+ state.name = item.name;
2065
+ state.arguments = item.arguments;
2066
+ } else {
2067
+ state.summary.clear();
2068
+ item.summary?.forEach((p, i) => state.summary.set(i, p.text));
2069
+ state.reasoningText = item.text ?? "";
2070
+ }
2071
+ };
2072
+ const applyContentPart = (outputIndex, itemId, contentIndex, part) => {
2073
+ const state = ensureState(outputIndex, itemId, "message");
2074
+ state.content.set(contentIndex, part);
2075
+ };
2076
+ const reconstructedOutput = () => [...outputs.entries()].sort(([a], [b]) => a - b).map(([outputIndex, state]) => {
2077
+ const canonical = canonicalOutputItems.get(outputIndex);
2078
+ if (canonical) return canonical;
2079
+ if (state.kind === "message") return { type: "message", ...state.itemId ? { id: state.itemId } : {}, ...state.status ? { status: state.status } : {}, role: state.role ?? "assistant", ...state.phase ? { phase: state.phase } : {}, content: [...state.content.entries()].sort(([a], [b]) => a - b).map(([, p]) => p) };
2080
+ if (state.kind === "function_call") return { type: "function_call", ...state.itemId ? { id: state.itemId } : {}, ...state.callId ? { call_id: state.callId } : {}, ...state.status ? { status: state.status } : {}, name: state.name ?? "", arguments: state.arguments };
2081
+ return { type: "reasoning", ...state.itemId ? { id: state.itemId } : {}, ...state.status ? { status: state.status } : {}, ...state.summary.size ? { summary: [...state.summary.entries()].sort(([a], [b]) => a - b).map(([, text]) => ({ type: "summary_text", text })) } : {}, ...state.reasoningText ? { text: state.reasoningText } : {} };
2082
+ });
2083
+ try {
2084
+ for await (const event of this.source) {
2085
+ if (event.data === "[DONE]") break;
2086
+ let payload;
2087
+ try {
2088
+ payload = JSON.parse(event.data);
2089
+ } catch (error) {
2090
+ throw new Error("Failed to parse OpenAI Responses SSE payload", { cause: error });
2091
+ }
2092
+ switch (payload.type) {
2093
+ case "response.created":
2094
+ case "response.in_progress":
2095
+ case "response.queued":
2096
+ responseMeta = payload.response;
2097
+ break;
2098
+ case "response.completed":
2099
+ case "response.done":
2100
+ finalResponse = payload.response;
2101
+ break;
2102
+ case "response.failed":
2103
+ terminalFailure = payload.response;
2104
+ finalResponse = terminalFailure;
2105
+ break;
2106
+ case "response.incomplete":
2107
+ terminalIncomplete = payload.response;
2108
+ finalResponse = terminalIncomplete;
2109
+ break;
2110
+ case "response.output_item.added": {
2111
+ const e = payload;
2112
+ applyOutputItem(e.output_index, e.item);
2113
+ break;
2114
+ }
2115
+ case "response.output_item.done": {
2116
+ const e = payload;
2117
+ applyOutputItem(e.output_index, e.item);
2118
+ canonicalOutputItems.set(e.output_index, e.item);
2119
+ break;
2120
+ }
2121
+ case "response.content_part.added":
2122
+ case "response.content_part.done": {
2123
+ const e = payload;
2124
+ if (e.part.type === "output_text" && "text" in e.part && typeof e.part.text === "string" || e.part.type === "refusal" && "refusal" in e.part && typeof e.part.refusal === "string") {
2125
+ applyContentPart(e.output_index, e.item_id, e.content_index, e.part);
2126
+ }
2127
+ break;
2128
+ }
2129
+ case "response.output_text.delta": {
2130
+ const e = payload;
2131
+ const s = ensureState(e.output_index, e.item_id, "message");
2132
+ const p = s.content.get(e.content_index);
2133
+ s.content.set(e.content_index, { type: "output_text", text: (p?.type === "output_text" ? p.text : "") + e.delta });
2134
+ break;
2135
+ }
2136
+ case "response.output_text.done": {
2137
+ const e = payload;
2138
+ const s = ensureState(e.output_index, e.item_id, "message");
2139
+ s.content.set(e.content_index, { type: "output_text", text: e.text });
2140
+ break;
2141
+ }
2142
+ case "response.refusal.delta": {
2143
+ const e = payload;
2144
+ const s = ensureState(e.output_index, e.item_id, "message");
2145
+ const p = s.content.get(e.content_index);
2146
+ s.content.set(e.content_index, { type: "refusal", refusal: (p?.type === "refusal" ? p.refusal : "") + e.delta });
2147
+ break;
2148
+ }
2149
+ case "response.refusal.done": {
2150
+ const e = payload;
2151
+ const s = ensureState(e.output_index, e.item_id, "message");
2152
+ s.content.set(e.content_index, { type: "refusal", refusal: e.refusal });
2153
+ break;
2154
+ }
2155
+ case "response.function_call_arguments.delta": {
2156
+ const e = payload;
2157
+ const s = ensureState(e.output_index, e.item_id, "function_call");
2158
+ s.arguments += e.delta;
2159
+ break;
2160
+ }
2161
+ case "response.function_call_arguments.done": {
2162
+ const e = payload;
2163
+ const s = ensureState(e.output_index, e.item_id, "function_call");
2164
+ s.arguments = e.arguments;
2165
+ if (e.name !== void 0) s.name = e.name;
2166
+ break;
2167
+ }
2168
+ case "response.reasoning_summary_part.added":
2169
+ case "response.reasoning_summary_part.done": {
2170
+ const e = payload;
2171
+ const s = ensureState(e.output_index, e.item_id, "reasoning");
2172
+ s.summary.set(e.summary_index, e.part.text);
2173
+ break;
2174
+ }
2175
+ case "response.reasoning_summary_text.delta": {
2176
+ const e = payload;
2177
+ const s = ensureState(e.output_index, e.item_id, "reasoning");
2178
+ s.summary.set(e.summary_index, (s.summary.get(e.summary_index) ?? "") + e.delta);
2179
+ break;
2180
+ }
2181
+ case "response.reasoning_summary_text.done": {
2182
+ const e = payload;
2183
+ const s = ensureState(e.output_index, e.item_id, "reasoning");
2184
+ s.summary.set(e.summary_index, e.text);
2185
+ break;
2186
+ }
2187
+ case "response.reasoning_text.delta": {
2188
+ const e = payload;
2189
+ const s = ensureState(e.output_index, e.item_id, "reasoning");
2190
+ s.reasoningText += e.delta;
2191
+ break;
2192
+ }
2193
+ case "response.reasoning_text.done": {
2194
+ const e = payload;
2195
+ const s = ensureState(e.output_index, e.item_id, "reasoning");
2196
+ s.reasoningText = e.text;
2197
+ break;
2198
+ }
2199
+ default:
2200
+ break;
2201
+ }
2202
+ yield payload;
2203
+ }
2204
+ if (terminalFailure || terminalIncomplete) {
2205
+ const response = terminalFailure ?? terminalIncomplete;
2206
+ const status = terminalFailure ? "failed" : "incomplete";
2207
+ const detail = response.error?.message ?? response.incomplete_details?.reason ?? "unknown error";
2208
+ throw new OpenAIResponsesStreamError("OpenAI Responses stream " + status + ": " + detail, response);
2209
+ }
2210
+ if (!finalResponse) {
2211
+ if (!responseMeta) throw new Error("OpenAI Responses stream ended without a terminal response payload");
2212
+ if (responseMeta.status === "failed" || responseMeta.status === "incomplete") {
2213
+ const detail = responseMeta.error?.message ?? responseMeta.incomplete_details?.reason ?? "unknown error";
2214
+ throw new OpenAIResponsesStreamError("OpenAI Responses stream " + responseMeta.status + ": " + detail, responseMeta);
2215
+ }
2216
+ finalResponse = { ...responseMeta, output: reconstructedOutput(), status: "completed" };
2217
+ } else if (finalResponse.status === "failed" || finalResponse.status === "incomplete") {
2218
+ const detail = finalResponse.error?.message ?? finalResponse.incomplete_details?.reason ?? "unknown error";
2219
+ throw new OpenAIResponsesStreamError("OpenAI Responses stream " + finalResponse.status + ": " + detail, finalResponse);
2220
+ }
2221
+ if (finalResponse.output.length === 0 && outputs.size > 0) finalResponse = { ...finalResponse, output: reconstructedOutput() };
2222
+ completed = true;
2223
+ this.removeAbortListener?.();
2224
+ this.removeAbortListener = void 0;
2225
+ this.resolveFinal(finalResponse);
2226
+ } catch (error) {
2227
+ this.removeAbortListener?.();
2228
+ this.removeAbortListener = void 0;
2229
+ this.rejectFinal(error);
2230
+ throw error;
2231
+ } finally {
2232
+ if (!completed) {
2233
+ this.source.abort?.();
2234
+ this.rejectFinal(new OllamaAbortError("OpenAI Responses stream ended before completion"));
2235
+ }
2236
+ }
2237
+ }
2238
+ };
2239
+ var OpenAICompatClient = class {
2240
+ constructor(http, runner) {
2241
+ this.http = http;
2242
+ this.runner = runner;
2243
+ }
2244
+ http;
2245
+ runner;
2246
+ request(operation, model, signal, holdUntil) {
2247
+ if (this.runner) {
2248
+ return this.runner(
2249
+ (http, runnerSignal) => operation(http, runnerSignal),
2250
+ {
2251
+ ...model !== void 0 ? { model } : {},
2252
+ ...signal !== void 0 ? { signal } : {},
2253
+ ...holdUntil !== void 0 ? { holdUntil } : {}
2254
+ }
2255
+ );
2256
+ }
2257
+ return operation(this.http, signal);
2258
+ }
2259
+ async createChatCompletion(request, signal) {
2260
+ if (request.stream) {
2261
+ return this.request(
2262
+ (http, requestSignal) => http.requestSseStream({
2263
+ path: "/v1/chat/completions",
2264
+ body: request,
2265
+ signal: requestSignal
2266
+ }).then((source) => new OpenAIChatCompletionStream(source, requestSignal)),
2267
+ request.model,
2268
+ signal,
2269
+ (stream) => stream.finalResult
2270
+ );
2271
+ }
2272
+ return this.request(
2273
+ (http, requestSignal) => http.request({
2274
+ path: "/v1/chat/completions",
2275
+ body: request,
2276
+ signal: requestSignal
2277
+ }),
2278
+ request.model,
2279
+ signal
2280
+ );
2281
+ }
2282
+ async chatCompletions(request, signal) {
2283
+ return this.createChatCompletion(request, signal);
2284
+ }
2285
+ async listModels(signal) {
2286
+ return this.request(
2287
+ (http, requestSignal) => http.request({
2288
+ path: "/v1/models",
2289
+ method: "GET",
2290
+ signal: requestSignal
2291
+ }),
2292
+ void 0,
2293
+ signal
2294
+ );
2295
+ }
2296
+ async retrieveModel(model, signal) {
2297
+ return this.request(
2298
+ (http, requestSignal) => http.request({
2299
+ path: `/v1/models/${encodeURIComponent(model)}`,
2300
+ method: "GET",
2301
+ signal: requestSignal
2302
+ }),
2303
+ model,
2304
+ signal
2305
+ );
2306
+ }
2307
+ async getModel(model, signal) {
2308
+ return this.retrieveModel(model, signal);
2309
+ }
2310
+ async createCompletion(request, signal) {
2311
+ if (request.stream) {
2312
+ return this.request(
2313
+ (http, requestSignal) => http.requestSseStream({
2314
+ path: "/v1/completions",
2315
+ body: request,
2316
+ signal: requestSignal
2317
+ }).then((source) => new OpenAICompletionStream(source, requestSignal)),
2318
+ request.model,
2319
+ signal,
2320
+ (stream) => stream.finalResult
2321
+ );
2322
+ }
2323
+ return this.request(
2324
+ (http, requestSignal) => http.request({
2325
+ path: "/v1/completions",
2326
+ body: request,
2327
+ signal: requestSignal
2328
+ }),
2329
+ request.model,
2330
+ signal
2331
+ );
2332
+ }
2333
+ async completions(request, signal) {
2334
+ return this.createCompletion(request, signal);
2335
+ }
2336
+ async createEmbedding(request, signal) {
2337
+ return this.request(
2338
+ (http, requestSignal) => http.request({
2339
+ path: "/v1/embeddings",
2340
+ body: request,
2341
+ signal: requestSignal
2342
+ }),
2343
+ request.model,
2344
+ signal
2345
+ );
2346
+ }
2347
+ async embeddings(request, signal) {
2348
+ return this.createEmbedding(request, signal);
2349
+ }
2350
+ async createResponses(request, signal) {
2351
+ if (request.stream) {
2352
+ return this.request(
2353
+ (http, requestSignal) => http.requestSseStream({
2354
+ path: "/v1/responses",
2355
+ body: request,
2356
+ signal: requestSignal
2357
+ }).then((source) => new OpenAIResponsesStream(source, requestSignal)),
2358
+ request.model,
2359
+ signal,
2360
+ (stream) => stream.finalResult
2361
+ );
2362
+ }
2363
+ return this.request(
2364
+ (http, requestSignal) => http.request({
2365
+ path: "/v1/responses",
2366
+ body: request,
2367
+ signal: requestSignal
2368
+ }),
2369
+ request.model,
2370
+ signal
2371
+ );
2372
+ }
2373
+ async responses(request, signal) {
2374
+ return this.createResponses(request, signal);
2375
+ }
2376
+ };
2377
+
2378
+ // src/integrations/anthropic.ts
2379
+ function isAnthropicKnownStreamEvent(event) {
2380
+ switch (event.type) {
2381
+ case "message_start":
2382
+ case "content_block_start":
2383
+ case "content_block_delta":
2384
+ case "content_block_stop":
2385
+ case "message_delta":
2386
+ case "message_stop":
2387
+ case "ping":
2388
+ case "error":
2389
+ return true;
2390
+ default:
2391
+ return false;
2392
+ }
2393
+ }
2394
+ function parseAnthropicEvent(event) {
2395
+ return JSON.parse(event.data);
2396
+ }
2397
+ function mergeContentBlock(current, delta) {
2398
+ if (delta.type === "text_delta" && current.type === "text") {
2399
+ return { ...current, text: current.text + delta.text };
2400
+ }
2401
+ if (delta.type === "input_json_delta" && current.type === "tool_use") {
2402
+ return current;
2403
+ }
2404
+ if (delta.type === "thinking_delta" && current.type === "thinking") {
2405
+ return { ...current, thinking: current.thinking + delta.thinking };
2406
+ }
2407
+ if (delta.type === "signature_delta" && current.type === "thinking") {
2408
+ return { ...current, signature: (current.signature ?? "") + delta.signature };
2409
+ }
2410
+ return current;
2411
+ }
2412
+ function sanitizeAnthropicTextBlock(block) {
2413
+ if (block.cache_control === void 0) return block;
2414
+ const { cache_control: _cacheControl, ...sanitized } = block;
2415
+ return sanitized;
2416
+ }
2417
+ function sanitizeAnthropicContentBlock(block) {
2418
+ if (block.type === "text") {
2419
+ return sanitizeAnthropicTextBlock(block);
2420
+ }
2421
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
2422
+ return {
2423
+ ...block,
2424
+ content: block.content.map(sanitizeAnthropicTextBlock)
2425
+ };
2426
+ }
2427
+ return block;
2428
+ }
2429
+ function sanitizeAnthropicRequest(request) {
2430
+ const messages = request.messages.map((message) => {
2431
+ if (typeof message.content === "string") return message;
2432
+ return {
2433
+ ...message,
2434
+ content: message.content.map(sanitizeAnthropicContentBlock)
2435
+ };
2436
+ });
2437
+ const system = Array.isArray(request.system) ? request.system.map(sanitizeAnthropicTextBlock) : request.system;
2438
+ const {
2439
+ tool_choice: _toolChoice,
2440
+ metadata: _metadata,
2441
+ ...sanitized
2442
+ } = request;
2443
+ return {
2444
+ ...sanitized,
2445
+ messages,
2446
+ ...system !== void 0 ? { system } : {}
2447
+ };
2448
+ }
2449
+ var AnthropicMessagesStream = class {
2450
+ constructor(source, signal) {
2451
+ this.source = source;
2452
+ this.finalResultPromise = new Promise((resolve, reject) => {
2453
+ this.resolveFinal = resolve;
2454
+ this.rejectFinal = reject;
2455
+ });
2456
+ if (signal !== void 0) {
2457
+ const onAbort = () => this.abort();
2458
+ if (signal.aborted) onAbort();
2459
+ else {
2460
+ signal.addEventListener("abort", onAbort, { once: true });
2461
+ this.removeAbortListener = () => signal.removeEventListener("abort", onAbort);
2462
+ }
2463
+ }
2464
+ }
2465
+ source;
2466
+ finalResultPromise;
2467
+ resolveFinal;
2468
+ rejectFinal;
2469
+ removeAbortListener;
2470
+ get finalResult() {
2471
+ return this.finalResultPromise;
2472
+ }
2473
+ abort() {
2474
+ this.removeAbortListener?.();
2475
+ this.removeAbortListener = void 0;
2476
+ this.source.abort?.();
2477
+ this.rejectFinal(new OllamaAbortError("Anthropic Messages stream aborted"));
2478
+ }
2479
+ async *[Symbol.asyncIterator]() {
2480
+ let message;
2481
+ const blocks = /* @__PURE__ */ new Map();
2482
+ const toolInputJson = /* @__PURE__ */ new Map();
2483
+ let completed = false;
2484
+ try {
2485
+ for await (const rawEvent of this.source) {
2486
+ let event;
2487
+ try {
2488
+ event = parseAnthropicEvent(rawEvent);
2489
+ } catch (error) {
2490
+ throw new Error("Failed to parse Anthropic message SSE payload", { cause: error });
2491
+ }
2492
+ if (!isAnthropicKnownStreamEvent(event)) {
2493
+ yield event;
2494
+ continue;
2495
+ }
2496
+ if (event.type === "error") {
2497
+ throw new Error(event.error.message);
2498
+ }
2499
+ if (event.type === "message_start") {
2500
+ message = {
2501
+ ...event.message,
2502
+ content: [],
2503
+ ...event.message.usage !== void 0 ? { usage: event.message.usage } : {}
2504
+ };
2505
+ } else if (event.type === "content_block_start") {
2506
+ blocks.set(event.index, event.content_block);
2507
+ } else if (event.type === "content_block_delta") {
2508
+ const current = blocks.get(event.index);
2509
+ if (event.delta.type === "input_json_delta") {
2510
+ toolInputJson.set(event.index, (toolInputJson.get(event.index) ?? "") + event.delta.partial_json);
2511
+ }
2512
+ if (current) blocks.set(event.index, mergeContentBlock(current, event.delta));
2513
+ } else if (event.type === "content_block_stop") {
2514
+ const current = blocks.get(event.index);
2515
+ if (current?.type === "tool_use") {
2516
+ const rawJson = toolInputJson.get(event.index);
2517
+ if (rawJson !== void 0 && rawJson !== "") {
2518
+ try {
2519
+ const parsed = JSON.parse(rawJson);
2520
+ blocks.set(event.index, { ...current, input: parsed });
2521
+ } catch {
2522
+ blocks.set(event.index, current);
2523
+ }
2524
+ }
2525
+ }
2526
+ } else if (event.type === "message_delta") {
2527
+ if (message) {
2528
+ message = {
2529
+ ...message,
2530
+ stop_reason: event.delta.stop_reason !== void 0 && event.delta.stop_reason !== null ? event.delta.stop_reason : message.stop_reason,
2531
+ stop_sequence: event.delta.stop_sequence ?? message.stop_sequence,
2532
+ ...event.usage !== void 0 ? {
2533
+ usage: {
2534
+ input_tokens: event.usage.input_tokens ?? message.usage?.input_tokens ?? 0,
2535
+ output_tokens: event.usage.output_tokens,
2536
+ ...event.usage.cache_creation_input_tokens !== void 0 ? { cache_creation_input_tokens: event.usage.cache_creation_input_tokens } : message.usage?.cache_creation_input_tokens !== void 0 ? { cache_creation_input_tokens: message.usage.cache_creation_input_tokens } : {},
2537
+ ...event.usage.cache_read_input_tokens !== void 0 ? { cache_read_input_tokens: event.usage.cache_read_input_tokens } : message.usage?.cache_read_input_tokens !== void 0 ? { cache_read_input_tokens: message.usage.cache_read_input_tokens } : {},
2538
+ ...event.usage.cache_creation !== void 0 ? { cache_creation: event.usage.cache_creation } : message.usage?.cache_creation !== void 0 ? { cache_creation: message.usage.cache_creation } : {}
2539
+ }
2540
+ } : {}
2541
+ };
2542
+ }
2543
+ } else if (event.type === "message_stop" && message) {
2544
+ message = {
2545
+ ...message,
2546
+ content: [...blocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block)
2547
+ };
2548
+ }
2549
+ yield event;
2550
+ }
2551
+ if (!message) {
2552
+ throw new Error("Anthropic Messages stream ended without message_start");
2553
+ }
2554
+ const final = {
2555
+ ...message,
2556
+ content: [...blocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block)
2557
+ };
2558
+ completed = true;
2559
+ this.removeAbortListener?.();
2560
+ this.removeAbortListener = void 0;
2561
+ this.resolveFinal(final);
2562
+ } catch (error) {
2563
+ this.removeAbortListener?.();
2564
+ this.removeAbortListener = void 0;
2565
+ this.rejectFinal(error);
2566
+ throw error;
2567
+ } finally {
2568
+ if (!completed) {
2569
+ this.source.abort?.();
2570
+ this.rejectFinal(new OllamaAbortError("Anthropic Messages stream ended before completion"));
2571
+ }
2572
+ }
2573
+ }
2574
+ };
2575
+ var AnthropicCompatClient = class {
2576
+ constructor(http, runner) {
2577
+ this.http = http;
2578
+ this.runner = runner;
2579
+ }
2580
+ http;
2581
+ runner;
2582
+ request(operation, model, signal, holdUntil) {
2583
+ if (this.runner) {
2584
+ return this.runner(
2585
+ (http, runnerSignal) => operation(http, runnerSignal),
2586
+ {
2587
+ model,
2588
+ ...signal !== void 0 ? { signal } : {},
2589
+ ...holdUntil !== void 0 ? { holdUntil } : {}
2590
+ }
2591
+ );
2592
+ }
2593
+ return operation(this.http, signal);
2594
+ }
2595
+ async createMessage(request, signal) {
2596
+ if (request.stream) {
2597
+ return this.request(
2598
+ (http, requestSignal) => http.requestSseStream({
2599
+ path: "/v1/messages",
2600
+ body: sanitizeAnthropicRequest(request),
2601
+ headers: { "anthropic-version": "2023-06-01" },
2602
+ signal: requestSignal
2603
+ }).then((source) => new AnthropicMessagesStream(source, requestSignal)),
2604
+ request.model,
2605
+ signal,
2606
+ (stream) => stream.finalResult
2607
+ );
2608
+ }
2609
+ return this.request(
2610
+ (http, requestSignal) => http.request({
2611
+ path: "/v1/messages",
2612
+ body: sanitizeAnthropicRequest(request),
2613
+ headers: { "anthropic-version": "2023-06-01" },
2614
+ signal: requestSignal
2615
+ }),
2616
+ request.model,
2617
+ signal
2618
+ );
1186
2619
  }
1187
2620
  async messages(request, signal) {
2621
+ if (request.stream) {
2622
+ return this.createMessage(request, signal);
2623
+ }
1188
2624
  return this.createMessage(request, signal);
1189
2625
  }
1190
2626
  };
1191
2627
 
2628
+ // src/generated/runtime/runtime.ts
2629
+ function compareVersions(a, b) {
2630
+ const pa = a.split(".").map((s) => Number.parseInt(s, 10) ?? 0);
2631
+ const pb = b.split(".").map((s) => Number.parseInt(s, 10) ?? 0);
2632
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
2633
+ const va = pa[i] ?? 0;
2634
+ const vb = pb[i] ?? 0;
2635
+ if (va !== vb) return va - vb;
2636
+ }
2637
+ return 0;
2638
+ }
2639
+ function assertOperationAllowed(operation, options) {
2640
+ const localMode = options.localMode ?? true;
2641
+ if (!localMode && !operation.environment.cloud) {
2642
+ throw new OllamaGenericClientError(
2643
+ `Operation ${operation.operationId} (${operation.method} ${operation.path}) is local-only and not supported in cloud mode.`
2644
+ );
2645
+ }
2646
+ if (operation.constraints?.minOllamaVersion && options.serverVersion && compareVersions(options.serverVersion, operation.constraints.minOllamaVersion) < 0) {
2647
+ throw new OllamaGenericClientError(
2648
+ `Operation ${operation.operationId} (${operation.method} ${operation.path}) requires Ollama >= ${operation.constraints.minOllamaVersion} (server reports ${options.serverVersion}).`
2649
+ );
2650
+ }
2651
+ }
2652
+ function shouldStream(req) {
2653
+ if (req.body && typeof req.body === "object" && "stream" in req.body) {
2654
+ const explicit = req.body.stream;
2655
+ if (typeof explicit === "boolean") return explicit;
2656
+ }
2657
+ return Boolean(req.streamingDefault ?? req.operation.transport.streamingDefault);
2658
+ }
2659
+ function buildBody(req) {
2660
+ if (req.body === void 0) return void 0;
2661
+ if (typeof req.body !== "object" || req.body === null) return req.body;
2662
+ const body = req.body;
2663
+ if (req.operation.transport.streaming && req.operation.transport.streamingDefault && body.stream === void 0) {
2664
+ return { ...body, stream: true };
2665
+ }
2666
+ return body;
2667
+ }
2668
+ var OllamaRuntime = class {
2669
+ constructor(options) {
2670
+ this.options = options;
2671
+ }
2672
+ options;
2673
+ /**
2674
+ * Invoke an operation. Called by every generated API method.
2675
+ *
2676
+ * Returns a `Promise<T>` for non-streaming operations, or an
2677
+ * `AsyncGenerator<T>` for streaming operations. The generated API
2678
+ * class's overloads pick the right return type at compile time.
2679
+ */
2680
+ async invoke(req) {
2681
+ assertOperationAllowed(req.operation, this.options);
2682
+ const body = buildBody(req);
2683
+ const streaming = shouldStream(req);
2684
+ const httpReq = {
2685
+ path: req.operation.path,
2686
+ method: req.operation.method,
2687
+ ...body !== void 0 ? { body } : {},
2688
+ ...req.signal !== void 0 ? { signal: req.signal } : {},
2689
+ // For streaming calls, request the raw Response so we can pipe it
2690
+ // through parseNdjsonStream directly. This inherits middleware,
2691
+ // retry, telemetry, and error-mapping from HttpClient — the
2692
+ // previous Wave 3 implementation bypassed them with a direct
2693
+ // `fetch()` call.
2694
+ ...streaming ? { raw: true } : {}
2695
+ };
2696
+ if (!streaming) {
2697
+ return await this.options.http.request(httpReq);
2698
+ }
2699
+ const response = await this.options.http.request(httpReq);
2700
+ if (!response.body) {
2701
+ throw new OllamaGenericClientError(
2702
+ `Operation ${req.operation.operationId}: streaming response had no body.`
2703
+ );
2704
+ }
2705
+ return parseNdjsonStream(response.body);
2706
+ }
2707
+ };
2708
+
1192
2709
  // src/utils.ts
1193
2710
  async function encodeImage(image) {
1194
2711
  if (typeof image === "string") return image;
@@ -1216,6 +2733,11 @@ async function withEncodedMessageImages(messages) {
1216
2733
  }
1217
2734
 
1218
2735
  // src/client.ts
2736
+ var logicalRequestSequence = 0;
2737
+ function createLogicalRequestId() {
2738
+ logicalRequestSequence += 1;
2739
+ return `ollama-request-${logicalRequestSequence}`;
2740
+ }
1219
2741
  var OllamaClient = class {
1220
2742
  registry;
1221
2743
  models;
@@ -1224,27 +2746,88 @@ var OllamaClient = class {
1224
2746
  failoverCodes;
1225
2747
  fetchImpl;
1226
2748
  logger;
2749
+ middleware;
2750
+ onLifecycleEvent;
2751
+ /**
2752
+ * API key for Ollama's hosted web tools (`webSearch`/`webFetch`), which always target
2753
+ * `OLLAMA_CLOUD_BASE_URL` regardless of `config.endpoints` — an Ollama account API key
2754
+ * is inherently a single global credential, not tied to any one inference endpoint.
2755
+ * Resolved the same way as a single-endpoint `apiKey` (`config.apiKey` falling back to
2756
+ * `OLLAMA_API_KEY`), independent of whether `config.endpoints` was used instead.
2757
+ */
2758
+ cloudApiKey;
1227
2759
  constructor(config = {}) {
1228
2760
  const resolvedApiKey = resolveApiKey(config.apiKey);
1229
- const endpoints = config.endpoints ?? [
2761
+ this.cloudApiKey = resolvedApiKey;
2762
+ const credentialEndpoints = resolveCredentialEndpoints(config);
2763
+ const endpoints = config.endpoints ?? (credentialEndpoints.length > 0 ? [] : [
1230
2764
  {
1231
2765
  name: "default",
1232
2766
  baseUrl: resolveBaseUrl(config.baseUrl),
1233
2767
  ...resolvedApiKey !== void 0 ? { apiKey: resolvedApiKey } : {},
1234
2768
  ...config.headers !== void 0 ? { headers: config.headers } : {}
1235
2769
  }
1236
- ];
1237
- this.registry = new EndpointRegistry(endpoints, config.endpointHealth);
2770
+ ]);
2771
+ this.registry = new EndpointRegistry(
2772
+ [...endpoints, ...credentialEndpoints],
2773
+ config.endpointHealth
2774
+ );
1238
2775
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1239
2776
  this.failoverCodes = new Set(config.failoverOn ?? DEFAULT_FAILOVER_CODES);
1240
2777
  this.fetchImpl = config.fetch ?? globalThis.fetch;
1241
2778
  this.logger = config.logger ?? (config.debug ? createConsoleLogger() : NOOP_LOGGER);
2779
+ this.middleware = config.middleware;
2780
+ this.onLifecycleEvent = config.onLifecycleEvent;
1242
2781
  this.retryConfig = typeof config.retries === "number" ? { ...DEFAULT_RETRY_CONFIG, maxRetries: config.retries } : { ...DEFAULT_RETRY_CONFIG, ...config.retries };
1243
2782
  this.models = new ModelsClient((op, opts) => this.executeWithFailover(op, opts));
1244
2783
  }
1245
2784
  get modelsClient() {
1246
2785
  return this.models;
1247
2786
  }
2787
+ /**
2788
+ * Return a generated {@link OllamaRuntime} that shares this client's
2789
+ * transport (HttpClient + middleware + retry + telemetry).
2790
+ *
2791
+ * Wave 8 (ADR 0018) bridge: lets callers mix the existing `OllamaClient`
2792
+ * API with the generated `NativeApi` / `OpenAIApi` / `AnthropicApi`
2793
+ * surface without configuring two separate HttpClient instances.
2794
+ *
2795
+ * The returned runtime is bound to the registry's first candidate
2796
+ * endpoint (or the resolved single-endpoint config if no `endpoints`
2797
+ * array was provided). For multi-endpoint configs that need per-call
2798
+ * routing, prefer constructing `OllamaRuntime` directly with a
2799
+ * specific HttpClient:
2800
+ *
2801
+ * ```ts
2802
+ * const http = new HttpClient({ baseUrl: 'http://host-a:11434' });
2803
+ * const runtime = new OllamaRuntime({ http });
2804
+ * const api = new NativeApi(runtime);
2805
+ * ```
2806
+ *
2807
+ * The runtime is cached on first call — subsequent calls return the
2808
+ * same instance.
2809
+ */
2810
+ get runtime() {
2811
+ if (this._runtime === void 0) {
2812
+ const candidates = this.registry.candidates();
2813
+ const endpoint = candidates[0];
2814
+ const baseUrl = endpoint?.baseUrl ?? "http://localhost:11434";
2815
+ const http = new HttpClient({
2816
+ baseUrl,
2817
+ ...endpoint?.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
2818
+ ...endpoint?.headers !== void 0 ? { headers: endpoint.headers } : {},
2819
+ fetch: this.fetchImpl,
2820
+ ...this.middleware !== void 0 ? { middleware: this.middleware } : {},
2821
+ ...this.onLifecycleEvent !== void 0 ? { onLifecycleEvent: this.onLifecycleEvent } : {}
2822
+ });
2823
+ this._runtime = new OllamaRuntime({
2824
+ http,
2825
+ localMode: inferRuntimeMode(baseUrl) === "local"
2826
+ });
2827
+ }
2828
+ return this._runtime;
2829
+ }
2830
+ _runtime;
1248
2831
  /**
1249
2832
  * Fail-fast guard for `format` (structured output) requests: throws before any network
1250
2833
  * call if the candidate endpoint is inferred as Ollama Cloud, which does not currently
@@ -1262,55 +2845,128 @@ var OllamaClient = class {
1262
2845
  }
1263
2846
  async executeWithFailover(operation, options) {
1264
2847
  const timeout = createTimeoutSignal(options?.timeoutMs ?? this.timeoutMs, options?.signal);
2848
+ const requestId = createLogicalRequestId();
2849
+ let deferTimeoutCancel = false;
1265
2850
  try {
1266
- const allCandidates = this.registry.candidates();
1267
- const candidates = options?.singleEndpoint ? allCandidates.slice(0, 1) : allCandidates;
1268
2851
  let lastError;
1269
- for (const [attemptIndex, endpoint] of candidates.entries()) {
1270
- this.logger.debug(`Executing on endpoint "${endpoint.name}" (${endpoint.baseUrl})`);
1271
- const http = new HttpClient({
1272
- baseUrl: endpoint.baseUrl,
1273
- ...endpoint.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
1274
- ...endpoint.headers !== void 0 ? { headers: endpoint.headers } : {},
1275
- fetch: this.fetchImpl
1276
- });
1277
- try {
1278
- const result = await withSpan(
1279
- "ollama.endpoint.attempt",
1280
- {
1281
- [ATTR_OLLAMA_ENDPOINT_NAME]: endpoint.name,
1282
- [ATTR_OLLAMA_ENDPOINT_ATTEMPT]: attemptIndex
1283
- },
1284
- () => withRetry(() => operation(http, timeout.signal), this.retryConfig)
2852
+ for (; ; ) {
2853
+ const allCandidates = this.registry.candidates(options?.model);
2854
+ if (allCandidates.length === 0 && options?.model !== void 0 && this.registry.hasModelScopedEndpoints()) {
2855
+ const configured = this.registry.list().flatMap((ep) => ep.models ?? []).filter((m, i, arr) => arr.indexOf(m) === i);
2856
+ throw new OllamaModelRoutingError(
2857
+ `No configured endpoint is authorized for model "${options.model}". ` + (configured.length > 0 ? `Endpoints are scoped to: ${configured.join(", ")}.` : "No endpoint declares a `models` allow-list that includes it.") + " Add or widen an `OllamaEndpoint.models` entry to route this model.",
2858
+ { model: options.model, availableModels: configured }
1285
2859
  );
1286
- this.registry.reportSuccess(endpoint.name);
1287
- return result;
1288
- } catch (err) {
1289
- const error = err instanceof Error ? err : new Error(String(err));
1290
- lastError = error;
1291
- this.registry.reportFailure(endpoint.name);
1292
- this.logger.warn(`Failed on "${endpoint.name}": ${error.message}`);
1293
- if (!(error instanceof OllamaClientError && this.failoverCodes.has(error.code)))
1294
- throw error;
1295
2860
  }
2861
+ const candidates = options?.singleEndpoint ? allCandidates.slice(0, 1) : allCandidates;
2862
+ const runnable = this.registry.filterWithCapacity(candidates);
2863
+ if (runnable.length === 0 && candidates.length > 0) {
2864
+ await this.registry.waitForCapacity(
2865
+ candidates.map((c) => c.name),
2866
+ timeout.signal
2867
+ );
2868
+ continue;
2869
+ }
2870
+ for (const [attemptIndex, endpoint] of runnable.entries()) {
2871
+ this.logger.debug(`Executing on endpoint "${endpoint.name}" (${endpoint.baseUrl})`);
2872
+ const http = new HttpClient({
2873
+ baseUrl: endpoint.baseUrl,
2874
+ ...endpoint.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
2875
+ ...endpoint.headers !== void 0 ? { headers: endpoint.headers } : {},
2876
+ fetch: this.fetchImpl,
2877
+ middleware: this.middleware,
2878
+ onLifecycleEvent: this.onLifecycleEvent,
2879
+ requestId
2880
+ });
2881
+ this.registry.acquire(endpoint.name);
2882
+ let holdPromise;
2883
+ try {
2884
+ const result = await withSpan(
2885
+ "ollama.endpoint.attempt",
2886
+ {
2887
+ [ATTR_OLLAMA_ENDPOINT_NAME]: endpoint.name,
2888
+ [ATTR_OLLAMA_ENDPOINT_ATTEMPT]: attemptIndex
2889
+ },
2890
+ () => withRetry(
2891
+ () => operation(http, timeout.signal),
2892
+ {
2893
+ ...this.retryConfig,
2894
+ onRetry: (error, attempt, delayMs) => {
2895
+ this.retryConfig.onRetry?.(error, attempt, delayMs);
2896
+ this.onLifecycleEvent?.({
2897
+ type: "retry",
2898
+ requestId,
2899
+ attempt: attempt + 1,
2900
+ error,
2901
+ delayMs,
2902
+ timestamp: Date.now()
2903
+ });
2904
+ }
2905
+ },
2906
+ timeout.signal
2907
+ )
2908
+ );
2909
+ this.registry.reportSuccess(endpoint.name);
2910
+ if (options?.holdUntil) {
2911
+ holdPromise = options.holdUntil(result);
2912
+ deferTimeoutCancel = true;
2913
+ }
2914
+ return result;
2915
+ } catch (err) {
2916
+ const error = err instanceof Error ? err : new Error(String(err));
2917
+ lastError = error;
2918
+ this.registry.reportFailure(endpoint.name);
2919
+ this.logger.warn(`Failed on "${endpoint.name}": ${error.message}`);
2920
+ if (!(error instanceof OllamaClientError && this.failoverCodes.has(error.code)))
2921
+ throw error;
2922
+ } finally {
2923
+ if (holdPromise) {
2924
+ void holdPromise.catch(() => void 0).finally(() => {
2925
+ timeout.cancel();
2926
+ this.registry.release(endpoint.name);
2927
+ });
2928
+ } else {
2929
+ this.registry.release(endpoint.name);
2930
+ }
2931
+ }
2932
+ }
2933
+ throw lastError ?? new Error("No healthy Ollama endpoints available");
1296
2934
  }
1297
- throw lastError ?? new Error("No healthy Ollama endpoints available");
1298
2935
  } finally {
1299
- timeout.cancel();
2936
+ if (!deferTimeoutCancel) timeout.cancel();
1300
2937
  }
1301
2938
  }
1302
2939
  async chat(req) {
1303
- const messages = await withEncodedMessageImages(req.messages);
2940
+ const encodedMessages = await withEncodedMessageImages(req.messages);
2941
+ const messages = encodedMessages.map((message) => {
2942
+ if (message.role === "tool" && message.tool_call_id !== void 0) {
2943
+ const { tool_call_id: _toolCallId, ...nativeMessage } = message;
2944
+ return nativeMessage;
2945
+ }
2946
+ return message;
2947
+ });
1304
2948
  if (req.stream) {
1305
- return this.executeWithFailover(async (http, signal) => {
1306
- if (req.format !== void 0) this.assertStructuredOutputSupported(http.baseUrl, req.model);
1307
- const stream = await http.requestStream({
1308
- path: "/api/chat",
1309
- body: { ...req, messages, stream: true },
1310
- signal
1311
- });
1312
- return normalizeChatStream(stream);
1313
- }, req);
2949
+ return this.executeWithFailover(
2950
+ async (http, signal) => {
2951
+ if (req.format !== void 0)
2952
+ this.assertStructuredOutputSupported(http.baseUrl, req.model);
2953
+ const stream = await http.requestStream({
2954
+ path: "/api/chat",
2955
+ body: { ...req, messages, stream: true },
2956
+ signal
2957
+ });
2958
+ return normalizeChatStream(stream, signal);
2959
+ },
2960
+ {
2961
+ signal: req.signal,
2962
+ timeoutMs: req.timeoutMs,
2963
+ model: req.model,
2964
+ // The endpoint's concurrency slot must stay held for as long as the stream is
2965
+ // actually being consumed, not just until the initial response/headers arrive
2966
+ // — see `executeWithFailover`'s `holdUntil` doc.
2967
+ holdUntil: (stream) => stream.finalResult
2968
+ }
2969
+ );
1314
2970
  }
1315
2971
  return withSpan(
1316
2972
  `chat ${req.model}`,
@@ -1354,16 +3010,24 @@ var OllamaClient = class {
1354
3010
  async generate(req) {
1355
3011
  const encodedReq = await withEncodedImages(req);
1356
3012
  if (encodedReq.stream) {
1357
- return this.executeWithFailover(async (http, signal) => {
1358
- if (encodedReq.format !== void 0)
1359
- this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
1360
- const stream = await http.requestStream({
1361
- path: "/api/generate",
1362
- body: { ...encodedReq, stream: true },
1363
- signal
1364
- });
1365
- return normalizeGenerateStream(stream);
1366
- }, encodedReq);
3013
+ return this.executeWithFailover(
3014
+ async (http, signal) => {
3015
+ if (encodedReq.format !== void 0)
3016
+ this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
3017
+ const stream = await http.requestStream({
3018
+ path: "/api/generate",
3019
+ body: { ...encodedReq, stream: true },
3020
+ signal
3021
+ });
3022
+ return normalizeGenerateStream(stream, signal);
3023
+ },
3024
+ {
3025
+ signal: encodedReq.signal,
3026
+ timeoutMs: encodedReq.timeoutMs,
3027
+ model: encodedReq.model,
3028
+ holdUntil: (stream) => stream.finalResult
3029
+ }
3030
+ );
1367
3031
  }
1368
3032
  return withSpan(
1369
3033
  `text_completion ${encodedReq.model}`,
@@ -1437,30 +3101,97 @@ var OllamaClient = class {
1437
3101
  createBlob = (digest, data) => this.models.createBlob(digest, data);
1438
3102
  checkBlob = (digest) => this.models.checkBlob(digest);
1439
3103
  // --- Web Endpoints ---
1440
- webSearch(req) {
1441
- return this.executeWithFailover(
1442
- (http, signal) => http.request({ path: "/api/websearch", body: req, signal }),
3104
+ /**
3105
+ * Ollama's hosted web search tool (`POST https://ollama.com/api/web_search`) — a fixed
3106
+ * Ollama Cloud service, unrelated to `config.endpoints`/`baseUrl` and not proxied
3107
+ * through a local server. Requires an Ollama account API key: pass `apiKey` to the
3108
+ * client constructor or set `OLLAMA_API_KEY`. Applies the same default `timeoutMs` and
3109
+ * retry policy as inference calls, but never cross-endpoint-fails-over — there is only
3110
+ * ever the one cloud endpoint to call.
3111
+ */
3112
+ async webSearch(req) {
3113
+ const { count, max_results, ...rest } = req;
3114
+ const res = await this.executeCloudRequest(
3115
+ (http, signal) => http.request({
3116
+ path: "/api/web_search",
3117
+ body: { ...rest, max_results: max_results ?? count },
3118
+ signal
3119
+ }),
1443
3120
  req
1444
3121
  );
3122
+ return { results: res.results.map((r) => ({ ...r, snippet: r.content })) };
1445
3123
  }
3124
+ /**
3125
+ * Ollama's hosted web fetch tool (`POST https://ollama.com/api/web_fetch`) — see
3126
+ * {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry behavior,
3127
+ * which applies identically here.
3128
+ */
1446
3129
  webFetch(req) {
1447
- return this.executeWithFailover(
1448
- (http, signal) => http.request({ path: "/api/webfetch", body: req, signal }),
3130
+ return this.executeCloudRequest(
3131
+ (http, signal) => http.request({ path: "/api/web_fetch", body: req, signal }),
1449
3132
  req
1450
3133
  );
1451
3134
  }
3135
+ /**
3136
+ * Runs `operation` against the fixed Ollama Cloud host with this client's default
3137
+ * timeout and retry policy — the same primitives `executeWithFailover` uses, minus the
3138
+ * multi-candidate loop, since `webSearch`/`webFetch` only ever have the one endpoint.
3139
+ */
3140
+ async executeCloudRequest(operation, options) {
3141
+ const timeout = createTimeoutSignal(options.timeoutMs ?? this.timeoutMs, options.signal);
3142
+ const requestId = createLogicalRequestId();
3143
+ try {
3144
+ const http = new HttpClient({
3145
+ baseUrl: OLLAMA_CLOUD_BASE_URL,
3146
+ apiKey: this.cloudApiKey,
3147
+ fetch: this.fetchImpl,
3148
+ middleware: this.middleware,
3149
+ onLifecycleEvent: this.onLifecycleEvent,
3150
+ requestId
3151
+ });
3152
+ return await withRetry(
3153
+ () => operation(http, timeout.signal),
3154
+ {
3155
+ ...this.retryConfig,
3156
+ onRetry: (error, attempt, delayMs) => {
3157
+ this.retryConfig.onRetry?.(error, attempt, delayMs);
3158
+ this.onLifecycleEvent?.({
3159
+ type: "retry",
3160
+ requestId,
3161
+ attempt: attempt + 1,
3162
+ error,
3163
+ delayMs,
3164
+ timestamp: Date.now()
3165
+ });
3166
+ }
3167
+ },
3168
+ timeout.signal
3169
+ );
3170
+ } finally {
3171
+ timeout.cancel();
3172
+ }
3173
+ }
1452
3174
  // --- Capabilities & Health ---
1453
- capabilities(model) {
1454
- return this.executeWithFailover((http) => detectModelCapabilities(http, model), {
1455
- singleEndpoint: true
1456
- });
3175
+ capabilities(model, signal) {
3176
+ return this.executeWithFailover(
3177
+ (http, runnerSignal) => detectModelCapabilities(http, model, runnerSignal),
3178
+ {
3179
+ singleEndpoint: true,
3180
+ model,
3181
+ ...signal !== void 0 ? { signal } : {}
3182
+ }
3183
+ );
1457
3184
  }
1458
3185
  runtimeMode() {
1459
3186
  const ep = this.registry.candidates()[0];
1460
3187
  return inferRuntimeMode(ep?.baseUrl ?? DEFAULT_BASE_URL);
1461
3188
  }
1462
3189
  healthCheck() {
1463
- return Promise.all(this.registry.list().map((ep) => checkEndpointHealth(ep, this.fetchImpl)));
3190
+ return Promise.all(
3191
+ this.registry.list().map(
3192
+ (ep) => checkEndpointHealth(ep, this.fetchImpl, 5e3, this.middleware, this.onLifecycleEvent)
3193
+ )
3194
+ );
1464
3195
  }
1465
3196
  endpointStatus() {
1466
3197
  return this.registry.status();
@@ -1473,8 +3204,11 @@ var OllamaClient = class {
1473
3204
  baseUrl: ep?.baseUrl ?? DEFAULT_BASE_URL,
1474
3205
  apiKey: ep?.apiKey,
1475
3206
  headers: ep?.headers,
1476
- fetch: this.fetchImpl
1477
- })
3207
+ fetch: this.fetchImpl,
3208
+ middleware: this.middleware,
3209
+ onLifecycleEvent: this.onLifecycleEvent
3210
+ }),
3211
+ (op, opts) => this.executeWithFailover(op, opts)
1478
3212
  );
1479
3213
  }
1480
3214
  get anthropic() {
@@ -1484,34 +3218,15 @@ var OllamaClient = class {
1484
3218
  baseUrl: ep?.baseUrl ?? DEFAULT_BASE_URL,
1485
3219
  apiKey: ep?.apiKey,
1486
3220
  headers: ep?.headers,
1487
- fetch: this.fetchImpl
1488
- })
3221
+ fetch: this.fetchImpl,
3222
+ middleware: this.middleware,
3223
+ onLifecycleEvent: this.onLifecycleEvent
3224
+ }),
3225
+ (op, opts) => this.executeWithFailover(op, opts)
1489
3226
  );
1490
3227
  }
1491
3228
  };
1492
3229
 
1493
- // src/middleware.ts
1494
- function composeMiddleware(middlewares, finalHandler) {
1495
- return function runPipeline(initialRequest) {
1496
- let index = -1;
1497
- function dispatch(i, currentRequest) {
1498
- if (i <= index) {
1499
- return Promise.reject(new Error("next() called multiple times in middleware"));
1500
- }
1501
- index = i;
1502
- const fn = middlewares[i];
1503
- if (i === middlewares.length || !fn) {
1504
- return finalHandler(currentRequest);
1505
- }
1506
- return fn({
1507
- request: currentRequest,
1508
- next: () => dispatch(i + 1, currentRequest)
1509
- });
1510
- }
1511
- return dispatch(0, initialRequest);
1512
- };
1513
- }
1514
-
1515
3230
  // src/streaming/adapters.ts
1516
3231
  function toTextStream(stream) {
1517
3232
  const iterator = stream[Symbol.asyncIterator]();
@@ -1570,6 +3285,117 @@ function toResponse(stream, init = {}) {
1570
3285
  return new Response(body, { ...init, headers });
1571
3286
  }
1572
3287
 
3288
+ // src/quota.ts
3289
+ var QuotaManager = class {
3290
+ now;
3291
+ windows;
3292
+ constructor(options) {
3293
+ if (options.windows.length === 0) {
3294
+ throw new Error("QuotaManager requires at least one window configuration.");
3295
+ }
3296
+ this.now = options.now ?? Date.now;
3297
+ this.windows = options.windows.map((config) => ({
3298
+ config,
3299
+ tokensUsed: 0,
3300
+ requestsMade: 0,
3301
+ windowStartedAt: this.now()
3302
+ }));
3303
+ }
3304
+ rollover(state) {
3305
+ if (this.now() - state.windowStartedAt >= state.config.windowMs) {
3306
+ state.tokensUsed = 0;
3307
+ state.requestsMade = 0;
3308
+ state.windowStartedAt = this.now();
3309
+ }
3310
+ }
3311
+ /** Current usage, budgets, and reset time for every configured window. */
3312
+ status() {
3313
+ return this.windows.map((state) => {
3314
+ this.rollover(state);
3315
+ const { config } = state;
3316
+ return {
3317
+ ...config,
3318
+ tokensUsed: state.tokensUsed,
3319
+ requestsMade: state.requestsMade,
3320
+ windowStartedAt: state.windowStartedAt,
3321
+ windowResetAt: state.windowStartedAt + config.windowMs,
3322
+ ...config.maxTokens !== void 0 ? { remainingTokens: Math.max(0, config.maxTokens - state.tokensUsed) } : {},
3323
+ ...config.maxRequests !== void 0 ? { remainingRequests: Math.max(0, config.maxRequests - state.requestsMade) } : {}
3324
+ };
3325
+ });
3326
+ }
3327
+ /**
3328
+ * True if one more request — optionally estimated at `estimatedTokens` — would stay
3329
+ * within every configured window's budget. Read-only; does not record usage.
3330
+ */
3331
+ canProceed(estimatedTokens = 0) {
3332
+ return this.windows.every((state) => {
3333
+ this.rollover(state);
3334
+ const { config } = state;
3335
+ if (config.maxRequests !== void 0 && state.requestsMade + 1 > config.maxRequests) {
3336
+ return false;
3337
+ }
3338
+ return !(config.maxTokens !== void 0 && state.tokensUsed + estimatedTokens > config.maxTokens);
3339
+ });
3340
+ }
3341
+ /**
3342
+ * Throws `OllamaQuotaExceededError` for the first window that `canProceed(estimatedTokens)`
3343
+ * would fail on; otherwise a no-op. Call before issuing a request.
3344
+ */
3345
+ assertCanProceed(estimatedTokens = 0) {
3346
+ for (const state of this.windows) {
3347
+ this.rollover(state);
3348
+ const { config } = state;
3349
+ if (config.maxRequests !== void 0 && state.requestsMade + 1 > config.maxRequests) {
3350
+ throw new OllamaQuotaExceededError(
3351
+ `Client-side quota window "${config.id}" would exceed its request budget (${state.requestsMade}/${config.maxRequests} requests used).`,
3352
+ { windowId: config.id, resetAt: state.windowStartedAt + config.windowMs }
3353
+ );
3354
+ }
3355
+ if (config.maxTokens !== void 0 && state.tokensUsed + estimatedTokens > config.maxTokens) {
3356
+ throw new OllamaQuotaExceededError(
3357
+ `Client-side quota window "${config.id}" would exceed its token budget (${state.tokensUsed}/${config.maxTokens} tokens used).`,
3358
+ { windowId: config.id, resetAt: state.windowStartedAt + config.windowMs }
3359
+ );
3360
+ }
3361
+ }
3362
+ }
3363
+ /** Records one request's actual usage — a raw API response or a pre-normalized `TokenUsage` — against every window. */
3364
+ recordUsage(usage) {
3365
+ const normalized = "totalTokens" in usage ? usage : extractUsage(usage);
3366
+ for (const state of this.windows) {
3367
+ this.rollover(state);
3368
+ state.tokensUsed += normalized.totalTokens;
3369
+ state.requestsMade += 1;
3370
+ }
3371
+ }
3372
+ /** Resets one window (by `id`) or, if omitted, all windows — zeroing usage and restarting their clocks. */
3373
+ reset(windowId) {
3374
+ for (const state of this.windows) {
3375
+ if (windowId === void 0 || state.config.id === windowId) {
3376
+ state.tokensUsed = 0;
3377
+ state.requestsMade = 0;
3378
+ state.windowStartedAt = this.now();
3379
+ }
3380
+ }
3381
+ }
3382
+ };
3383
+ function createOllamaCloudFreeTierQuota(budgets) {
3384
+ const windows = [];
3385
+ if (budgets.session !== void 0) {
3386
+ windows.push({ id: "session", windowMs: 5 * 60 * 60 * 1e3, ...budgets.session });
3387
+ }
3388
+ if (budgets.weekly !== void 0) {
3389
+ windows.push({ id: "weekly", windowMs: 7 * 24 * 60 * 60 * 1e3, ...budgets.weekly });
3390
+ }
3391
+ if (windows.length === 0) {
3392
+ throw new Error(
3393
+ "createOllamaCloudFreeTierQuota requires at least a `session` or `weekly` budget."
3394
+ );
3395
+ }
3396
+ return new QuotaManager({ windows, ...budgets.now !== void 0 ? { now: budgets.now } : {} });
3397
+ }
3398
+
1573
3399
  // src/tools/define-tool.ts
1574
3400
  function defineTool(options) {
1575
3401
  const jsonSchema = zodToJsonSchema(options.schema);
@@ -1764,28 +3590,234 @@ var ToolRegistry = class {
1764
3590
  };
1765
3591
  }
1766
3592
  };
1767
- function convertMcpDescriptorToTool(descriptor, mcpClient, namePrefix = "") {
3593
+
3594
+ // src/mcp/json-schema.ts
3595
+ function label(path) {
3596
+ return path.length === 0 ? "value" : "value" + path.map((part) => typeof part === "number" ? "[" + part + "]" : "." + part).join("");
3597
+ }
3598
+ function equalJson(a, b) {
3599
+ if (Object.is(a, b)) return true;
3600
+ if (typeof a !== typeof b || a === null || b === null) return false;
3601
+ if (Array.isArray(a) || Array.isArray(b)) {
3602
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => equalJson(item, b[i]));
3603
+ }
3604
+ if (typeof a === "object" && typeof b === "object") {
3605
+ const ar = a;
3606
+ const br = b;
3607
+ const ak = Object.keys(ar).sort();
3608
+ const bk = Object.keys(br).sort();
3609
+ return ak.length === bk.length && ak.every((key, i) => key === bk[i] && equalJson(ar[key], br[key]));
3610
+ }
3611
+ return false;
3612
+ }
3613
+ function matchesType(value, type) {
3614
+ switch (type) {
3615
+ case "null":
3616
+ return value === null;
3617
+ case "boolean":
3618
+ return typeof value === "boolean";
3619
+ case "object":
3620
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3621
+ case "array":
3622
+ return Array.isArray(value);
3623
+ case "string":
3624
+ return typeof value === "string";
3625
+ case "number":
3626
+ return typeof value === "number" && Number.isFinite(value);
3627
+ case "integer":
3628
+ return typeof value === "number" && Number.isInteger(value);
3629
+ default:
3630
+ return true;
3631
+ }
3632
+ }
3633
+ function validate(value, schema, path) {
3634
+ if (schema === true || schema === void 0) return [];
3635
+ if (schema === false) return [{ path, message: label(path) + " is not allowed by the schema" }];
3636
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) return [];
3637
+ const s = schema;
3638
+ const issues = [];
3639
+ if (s["nullable"] === true && value === null) return issues;
3640
+ const type = s["type"];
3641
+ if (typeof type === "string" && !matchesType(value, type)) {
3642
+ return [{ path, message: label(path) + " must be " + type }];
3643
+ }
3644
+ if (Array.isArray(type) && !type.some((t) => typeof t === "string" && matchesType(value, t))) {
3645
+ return [{ path, message: label(path) + " must match one of: " + type.join(", ") }];
3646
+ }
3647
+ if ("const" in s && !equalJson(value, s["const"])) issues.push({ path, message: label(path) + " must equal the schema const value" });
3648
+ const enumValues = s["enum"];
3649
+ if (Array.isArray(enumValues) && !enumValues.some((item) => equalJson(value, item))) issues.push({ path, message: label(path) + " must be one of the allowed enum values" });
3650
+ const anyOf = s["anyOf"];
3651
+ if (Array.isArray(anyOf) && !anyOf.some((candidate) => validate(value, candidate, path).length === 0)) issues.push({ path, message: label(path) + " must satisfy at least one anyOf schema" });
3652
+ const oneOf = s["oneOf"];
3653
+ if (Array.isArray(oneOf)) {
3654
+ const matches = oneOf.filter((candidate) => validate(value, candidate, path).length === 0).length;
3655
+ if (matches !== 1) issues.push({ path, message: label(path) + " must satisfy exactly one oneOf schema" });
3656
+ }
3657
+ const allOf = s["allOf"];
3658
+ if (Array.isArray(allOf)) for (const candidate of allOf) issues.push(...validate(value, candidate, path));
3659
+ if ("not" in s && validate(value, s["not"], path).length === 0) issues.push({ path, message: label(path) + " must not satisfy the not schema" });
3660
+ if (typeof value === "string") {
3661
+ const min = s["minLength"];
3662
+ const max = s["maxLength"];
3663
+ if (typeof min === "number" && value.length < min) issues.push({ path, message: label(path) + " must have at least " + min + " characters" });
3664
+ if (typeof max === "number" && value.length > max) issues.push({ path, message: label(path) + " must have at most " + max + " characters" });
3665
+ if (typeof s["pattern"] === "string") {
3666
+ try {
3667
+ if (!new RegExp(s["pattern"]).test(value)) issues.push({ path, message: label(path) + " does not match the required pattern" });
3668
+ } catch {
3669
+ }
3670
+ }
3671
+ }
3672
+ if (typeof value === "number" && Number.isFinite(value)) {
3673
+ const numericRules = [
3674
+ ["minimum", (n, b) => n >= b, "must be greater than or equal to"],
3675
+ ["maximum", (n, b) => n <= b, "must be less than or equal to"],
3676
+ ["exclusiveMinimum", (n, b) => n > b, "must be greater than"],
3677
+ ["exclusiveMaximum", (n, b) => n < b, "must be less than"]
3678
+ ];
3679
+ for (const [keyword, check, phrase] of numericRules) {
3680
+ const bound = s[keyword];
3681
+ if (typeof bound === "number" && !check(value, bound)) issues.push({ path, message: label(path) + " " + phrase + " " + bound });
3682
+ }
3683
+ const multipleOf = s["multipleOf"];
3684
+ if (typeof multipleOf === "number" && multipleOf > 0) {
3685
+ const quotient = value / multipleOf;
3686
+ if (Math.abs(quotient - Math.round(quotient)) > 1e-12) issues.push({ path, message: label(path) + " must be a multiple of " + multipleOf });
3687
+ }
3688
+ }
3689
+ if (Array.isArray(value)) {
3690
+ const min = s["minItems"];
3691
+ const max = s["maxItems"];
3692
+ if (typeof min === "number" && value.length < min) issues.push({ path, message: label(path) + " must contain at least " + min + " items" });
3693
+ if (typeof max === "number" && value.length > max) issues.push({ path, message: label(path) + " must contain at most " + max + " items" });
3694
+ if (s["uniqueItems"] === true) {
3695
+ for (let i = 0; i < value.length; i += 1) if (value.slice(0, i).some((previous) => equalJson(previous, value[i]))) {
3696
+ issues.push({ path: [...path, i], message: label([...path, i]) + " must be unique" });
3697
+ break;
3698
+ }
3699
+ }
3700
+ if ("items" in s) value.forEach((item, i) => issues.push(...validate(item, s["items"], [...path, i])));
3701
+ if (Array.isArray(s["prefixItems"])) s["prefixItems"].forEach((itemSchema, i) => {
3702
+ if (i < value.length) issues.push(...validate(value[i], itemSchema, [...path, i]));
3703
+ });
3704
+ }
3705
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3706
+ const object = value;
3707
+ const required = s["required"];
3708
+ if (Array.isArray(required)) {
3709
+ for (const key of required) if (typeof key === "string" && !(key in object)) issues.push({ path: [...path, key], message: label([...path, key]) + " is required" });
3710
+ }
3711
+ const properties = s["properties"];
3712
+ if (typeof properties === "object" && properties !== null && !Array.isArray(properties)) {
3713
+ const propertySchemas = properties;
3714
+ for (const [key, propertySchema] of Object.entries(propertySchemas)) if (key in object) issues.push(...validate(object[key], propertySchema, [...path, key]));
3715
+ const additional = s["additionalProperties"];
3716
+ for (const key of Object.keys(object)) {
3717
+ if (key in propertySchemas) continue;
3718
+ if (additional === false) issues.push({ path: [...path, key], message: label([...path, key]) + " is not allowed" });
3719
+ else if (additional !== true && additional !== void 0) issues.push(...validate(object[key], additional, [...path, key]));
3720
+ }
3721
+ }
3722
+ const min = s["minProperties"];
3723
+ const max = s["maxProperties"];
3724
+ if (typeof min === "number" && Object.keys(object).length < min) issues.push({ path, message: label(path) + " must contain at least " + min + " properties" });
3725
+ if (typeof max === "number" && Object.keys(object).length > max) issues.push({ path, message: label(path) + " must contain at most " + max + " properties" });
3726
+ }
3727
+ return issues;
3728
+ }
3729
+ function validateJsonSchema(value, schema) {
3730
+ return validate(value, schema, []);
3731
+ }
3732
+
3733
+ // src/mcp/mcp-tools.ts
3734
+ function resolveMaxPages(options) {
3735
+ const maxPages = options.maxPages ?? 64;
3736
+ if (!Number.isInteger(maxPages) || maxPages <= 0) {
3737
+ throw new RangeError("MCP maxPages must be a positive integer");
3738
+ }
3739
+ return maxPages;
3740
+ }
3741
+ async function listAllMcpTools(mcpClient, options = {}, signal) {
3742
+ const maxPages = resolveMaxPages(options);
3743
+ const tools = [];
3744
+ const seenCursors = /* @__PURE__ */ new Set();
3745
+ let cursor;
3746
+ for (let pageNumber = 1; pageNumber <= maxPages; pageNumber += 1) {
3747
+ const params = cursor !== void 0 ? { cursor } : void 0;
3748
+ const requestOptions = signal !== void 0 ? { signal } : void 0;
3749
+ const page = await mcpClient.listTools(params, requestOptions);
3750
+ tools.push(...page.tools);
3751
+ if (page.nextCursor === void 0) {
3752
+ return tools;
3753
+ }
3754
+ if (seenCursors.has(page.nextCursor)) {
3755
+ throw new Error("MCP tools/list returned a repeated nextCursor");
3756
+ }
3757
+ seenCursors.add(page.nextCursor);
3758
+ cursor = page.nextCursor;
3759
+ }
3760
+ throw new Error("MCP tools/list exceeded maxPages (" + maxPages + ")");
3761
+ }
3762
+ function formatMcpToolResult(result) {
3763
+ const parts = result.content.map((block) => {
3764
+ if (block.type === "text" && typeof block.text === "string") {
3765
+ return block.text;
3766
+ }
3767
+ try {
3768
+ return JSON.stringify(block);
3769
+ } catch {
3770
+ return `[${block.type}]`;
3771
+ }
3772
+ });
3773
+ if (result.structuredContent !== void 0) {
3774
+ try {
3775
+ parts.push(JSON.stringify(result.structuredContent));
3776
+ } catch {
3777
+ }
3778
+ }
3779
+ if (result.isError) {
3780
+ parts.unshift("[MCP tool error]");
3781
+ }
3782
+ return parts.join("\n");
3783
+ }
3784
+ function createInputValidator(inputSchema) {
3785
+ return z.custom().superRefine((value, ctx) => {
3786
+ const issues = validateJsonSchema(value, inputSchema);
3787
+ for (const issue of issues) {
3788
+ ctx.addIssue({
3789
+ code: z.ZodIssueCode.custom,
3790
+ path: [...issue.path],
3791
+ message: issue.message
3792
+ });
3793
+ }
3794
+ });
3795
+ }
3796
+ function convertMcpDescriptorToTool(descriptor, mcpClient, namePrefix = "", resultMode = "text") {
1768
3797
  const toolName = `${namePrefix}${descriptor.name}`;
1769
- const inputSchema = descriptor.inputSchema ?? { properties: {} };
3798
+ const inputSchema = descriptor.inputSchema ?? { type: "object", properties: {} };
1770
3799
  const properties = inputSchema["properties"] ?? {};
1771
3800
  const required = inputSchema["required"] ?? [];
3801
+ const parameters = {
3802
+ ...inputSchema,
3803
+ type: "object",
3804
+ properties,
3805
+ ...required.length > 0 ? { required } : {}
3806
+ };
1772
3807
  return {
1773
3808
  name: toolName,
1774
3809
  description: descriptor.description ?? "",
1775
- schema: z.record(z.string(), z.unknown()),
1776
- execute: async (args) => {
3810
+ schema: createInputValidator(inputSchema),
3811
+ execute: async (args, context) => {
1777
3812
  try {
1778
- const result = await mcpClient.callTool({
1779
- name: descriptor.name,
1780
- arguments: args
1781
- });
1782
- if (result.isError) {
1783
- throw new OllamaMcpError(`MCP tool "${descriptor.name}" returned error`, {
1784
- mcpMethod: "callTool",
1785
- toolName: descriptor.name
1786
- });
1787
- }
1788
- return result.content.map((b) => b.type === "text" && b.text ? b.text : `[${b.type}]`).join("\n");
3813
+ const result = await mcpClient.callTool(
3814
+ {
3815
+ name: descriptor.name,
3816
+ arguments: args
3817
+ },
3818
+ context.signal !== void 0 ? { signal: context.signal } : void 0
3819
+ );
3820
+ return resultMode === "structured" ? result : formatMcpToolResult(result);
1789
3821
  } catch (err) {
1790
3822
  if (err instanceof OllamaMcpError) throw err;
1791
3823
  throw new OllamaMcpError(`Failed calling MCP tool "${descriptor.name}"`, {
@@ -1800,19 +3832,15 @@ function convertMcpDescriptorToTool(descriptor, mcpClient, namePrefix = "") {
1800
3832
  function: {
1801
3833
  name: toolName,
1802
3834
  description: descriptor.description ?? "",
1803
- parameters: {
1804
- type: "object",
1805
- properties,
1806
- ...required.length > 0 ? { required } : {}
1807
- }
3835
+ parameters
1808
3836
  }
1809
3837
  }
1810
3838
  };
1811
3839
  }
1812
- async function loadMcpTools(mcpClient, options = {}) {
3840
+ async function loadMcpTools(mcpClient, options = {}, signal) {
1813
3841
  try {
1814
- const { tools } = await mcpClient.listTools();
1815
- return tools.map((t) => convertMcpDescriptorToTool(t, mcpClient, options.namePrefix));
3842
+ const tools = await listAllMcpTools(mcpClient, options, signal);
3843
+ return tools.map((t) => convertMcpDescriptorToTool(t, mcpClient, options.namePrefix, options.resultMode));
1816
3844
  } catch (err) {
1817
3845
  throw new OllamaMcpError("Failed listing MCP tools from client", {
1818
3846
  mcpMethod: "listTools",
@@ -1820,11 +3848,48 @@ async function loadMcpTools(mcpClient, options = {}) {
1820
3848
  });
1821
3849
  }
1822
3850
  }
1823
- async function registerMcpTools(registry, mcpClient, options = {}) {
1824
- const tools = await loadMcpTools(mcpClient, options);
3851
+ async function registerMcpTools(registry, mcpClient, options = {}, signal) {
3852
+ const tools = await loadMcpTools(mcpClient, options, signal);
1825
3853
  registry.registerMany(tools);
1826
3854
  }
1827
3855
 
3856
+ // src/mcp/bridge.ts
3857
+ var McpBridge = class _McpBridge {
3858
+ constructor(client, options = {}) {
3859
+ this.client = client;
3860
+ this.options = options;
3861
+ }
3862
+ client;
3863
+ options;
3864
+ static toOllamaTools(tools, options = {}) {
3865
+ const prefix = options.namePrefix ?? "";
3866
+ return tools.map((tool) => ({
3867
+ type: "function",
3868
+ function: {
3869
+ name: prefix + tool.name,
3870
+ description: tool.description ?? "",
3871
+ parameters: {
3872
+ ...tool.inputSchema ?? { type: "object", properties: {} },
3873
+ type: "object",
3874
+ properties: (tool.inputSchema ?? {})["properties"] ?? {}
3875
+ }
3876
+ }
3877
+ }));
3878
+ }
3879
+ async listTools(signal) {
3880
+ return listAllMcpTools(this.client, this.options, signal);
3881
+ }
3882
+ async definitions(signal) {
3883
+ return _McpBridge.toOllamaTools(await this.listTools(signal), this.options);
3884
+ }
3885
+ async loadTools(signal) {
3886
+ return loadMcpTools(this.client, this.options, signal);
3887
+ }
3888
+ async register(registry, signal) {
3889
+ await registry.registerMany(await this.loadTools(signal));
3890
+ }
3891
+ };
3892
+
1828
3893
  // src/skills/frontmatter.ts
1829
3894
  function parseYamlValue(raw) {
1830
3895
  const trimmed = raw.trim();
@@ -1926,12 +3991,24 @@ var Agent = class {
1926
3991
  client;
1927
3992
  tools;
1928
3993
  maxIterations;
3994
+ maxToolCalls;
1929
3995
  hooks;
3996
+ validateToolCapability;
3997
+ toolContextSize;
1930
3998
  constructor(client, config = {}) {
1931
3999
  this.client = client;
1932
4000
  this.tools = config.tools;
1933
4001
  this.maxIterations = config.maxIterations ?? 10;
4002
+ this.maxToolCalls = config.maxToolCalls;
1934
4003
  this.hooks = config.hooks;
4004
+ if (this.maxToolCalls !== void 0 && (!Number.isInteger(this.maxToolCalls) || this.maxToolCalls < 0)) {
4005
+ throw new RangeError("Agent maxToolCalls must be a non-negative integer");
4006
+ }
4007
+ this.validateToolCapability = config.validateToolCapability ?? true;
4008
+ this.toolContextSize = config.toolContextSize ?? 32768;
4009
+ if (!Number.isInteger(this.toolContextSize) || this.toolContextSize <= 0) {
4010
+ throw new RangeError("Agent toolContextSize must be a positive integer");
4011
+ }
1935
4012
  }
1936
4013
  run(input) {
1937
4014
  return withSpan(
@@ -1949,6 +4026,30 @@ var Agent = class {
1949
4026
  const history = [...input.messages];
1950
4027
  const turns = [];
1951
4028
  const toolDefs = this.tools?.definitions();
4029
+ const hasTools = toolDefs !== void 0 && toolDefs.length > 0;
4030
+ let capabilities;
4031
+ if (hasTools && this.validateToolCapability && this.client.capabilities) {
4032
+ capabilities = await this.client.capabilities(input.model, input.signal);
4033
+ if (!capabilities.supportsTools) {
4034
+ throw new OllamaIncompatibleModelError(
4035
+ `Model "${input.model}" does not advertise the "tools" capability.`,
4036
+ {
4037
+ model: input.model,
4038
+ capability: "tools",
4039
+ reportedCapabilities: capabilities.reported
4040
+ }
4041
+ );
4042
+ }
4043
+ }
4044
+ const shouldAutoSizeContext = hasTools && this.validateToolCapability && capabilities !== void 0 && input.options?.num_ctx === void 0;
4045
+ const effectiveOptions = shouldAutoSizeContext ? {
4046
+ ...input.options ?? {},
4047
+ num_ctx: Math.min(
4048
+ this.toolContextSize,
4049
+ capabilities?.contextLength ?? this.toolContextSize
4050
+ )
4051
+ } : input.options;
4052
+ let toolCallsExecuted = 0;
1952
4053
  for (let iteration = 1; iteration <= this.maxIterations; iteration++) {
1953
4054
  const outcome = await withSpan(
1954
4055
  "ollama.agent.turn",
@@ -1959,7 +4060,7 @@ var Agent = class {
1959
4060
  model: input.model,
1960
4061
  messages: history,
1961
4062
  ...toolDefs !== void 0 ? { tools: toolDefs } : {},
1962
- ...input.options !== void 0 ? { options: input.options } : {},
4063
+ ...effectiveOptions !== void 0 ? { options: effectiveOptions } : {},
1963
4064
  ...input.think !== void 0 ? { think: input.think } : {},
1964
4065
  stream: false,
1965
4066
  ...input.signal !== void 0 ? { signal: input.signal } : {}
@@ -1974,17 +4075,24 @@ var Agent = class {
1974
4075
  this.hooks?.onTurnEnd?.(finalTurn);
1975
4076
  return { done: true, finalMessage: assistantMessage };
1976
4077
  }
4078
+ if (this.maxToolCalls !== void 0 && toolCallsExecuted + toolCalls.length > this.maxToolCalls) {
4079
+ throw new OllamaAgentMaxToolCallsError(
4080
+ `Agent exceeded max tool calls (${this.maxToolCalls})`,
4081
+ { maxToolCalls: this.maxToolCalls, toolCallsExecuted }
4082
+ );
4083
+ }
1977
4084
  for (const tc of toolCalls) {
1978
4085
  this.hooks?.onToolCallStart?.(tc);
1979
4086
  }
1980
4087
  const toolResults = await this.tools.executeToolCalls(toolCalls, {
1981
4088
  signal: input.signal
1982
4089
  });
4090
+ toolCallsExecuted += toolCalls.length;
1983
4091
  for (const res of toolResults) {
1984
4092
  this.hooks?.onToolCallEnd?.(res);
1985
4093
  history.push({
1986
4094
  role: "tool",
1987
- ...res.toolCallId !== void 0 ? { tool_call_id: res.toolCallId } : {},
4095
+ tool_name: res.toolName,
1988
4096
  content: res.outputString
1989
4097
  });
1990
4098
  }
@@ -2005,6 +4113,6 @@ var Agent = class {
2005
4113
  }
2006
4114
  };
2007
4115
 
2008
- 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, encodeImage, extractUsage, inferRuntimeMode, listAvailableModels, loadMcpTools, mapError, normalizeChatStream, normalizeGenerateStream, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseStructuredOutput, registerMcpTools, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
4116
+ 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, AnthropicMessagesStream, DEFAULT_BACKOFF, DEFAULT_BASE_URL, DEFAULT_FAILOVER_CODES, DEFAULT_RETRY_CONFIG, DEFAULT_TIMEOUT_MS, EndpointRegistry, GEN_AI_SYSTEM_OLLAMA, HttpClient, McpBridge, ModelsClient, NANOS_PER_MS, NANOS_PER_SECOND, NOOP_LOGGER, OLLAMA_CLOUD_BASE_URL, OllamaAbortError, OllamaAgentMaxIterationsError, OllamaAgentMaxToolCallsError, OllamaAuthError, OllamaClient, OllamaClientError, OllamaGenericClientError, OllamaIncompatibleModelError, OllamaMcpError, OllamaModelRoutingError, OllamaNetworkError, OllamaNotFoundError, OllamaQuotaExceededError, OllamaRateLimitError, OllamaServerError, OllamaSkillInvalidError, OllamaSkillNotFoundError, OllamaStream, OllamaTimeoutError, OllamaToolTimeoutError, OllamaToolValidationError, OllamaUnsupportedCapabilityError, OpenAIChatCompletionStream, OpenAICompatClient, OpenAICompletionStream, OpenAIResponsesStream, OpenAIResponsesStreamError, QuotaManager, ToolRegistry, addSpanEvent, applySkill, calculateBackoff, checkEndpointHealth, composeMiddleware, createConsoleLogger, createOllamaCloudFreeTierQuota, createTimeoutSignal, defineTool, detectModelCapabilities, encodeImage, extractUsage, inferRuntimeMode, listAllMcpTools, listAvailableModels, loadMcpTools, mapError, normalizeChatStream, normalizeGenerateStream, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseSseStream, parseStructuredOutput, registerMcpTools, resolveCredentialEndpoints, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
2009
4117
  //# sourceMappingURL=index.js.map
2010
4118
  //# sourceMappingURL=index.js.map