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