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