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