@orkestrel/ollama 0.0.12 → 0.0.14

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.
@@ -1,29 +1,33 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _orkestrel_agent = require("@orkestrel/agent");
3
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_agent = require("@orkestrel/agent");
4
4
  let _orkestrel_ndjson = require("@orkestrel/ndjson");
5
5
  let _orkestrel_timeout = require("@orkestrel/timeout");
6
6
  //#region src/server/constants.ts
7
- /** The local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */
7
+ /** Names the local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */
8
8
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
9
9
  /**
10
- * How long the model stays resident after a call when `OllamaOptions.keepAlive` is
10
+ * Names how long the model stays resident after a call when `OllamaOptions.keepAlive` is
11
11
  * omitted — Ollama's own `keep_alive` default, expressed as a duration string.
12
+ *
13
+ * @remarks
14
+ * The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so
15
+ * the constant, the `OllamaOptions.keepAlive` key, and the wire member read as one term.
12
16
  */
13
17
  var DEFAULT_KEEP_ALIVE = "5m";
14
18
  /**
15
- * The per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —
19
+ * Names the per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —
16
20
  * generous enough that a cold model load does not trip it.
17
21
  */
18
22
  var DEFAULT_PROVIDER_TIMEOUT = 12e4;
19
23
  /**
20
- * The cap, in characters, on how much of a non-OK response body is
24
+ * Names the cap, in characters, on how much of a non-OK response body is
21
25
  * incorporated into a thrown {@link OllamaHTTPError}'s message.
22
26
  *
23
27
  * @remarks
24
28
  * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing
25
29
  * back an unbounded response body cannot inflate the thrown error's message
26
- * without limit (§14). `2048` characters is generous enough to carry a
30
+ * without limit. `2048` characters is generous enough to carry a
27
31
  * useful diagnostic snippet while staying well short of any practical size
28
32
  * concern.
29
33
  */
@@ -31,14 +35,15 @@ var MAX_ERROR_BODY_LENGTH = 2048;
31
35
  //#endregion
32
36
  //#region src/server/errors.ts
33
37
  /**
34
- * An error thrown when the Ollama `/api/chat` HTTP transport fails.
38
+ * Represents an error thrown when the Ollama `/api/chat` HTTP transport fails.
35
39
  *
36
40
  * @remarks
37
- * Carries the response `status` (0 when no HTTP response was received at all,
38
- * e.g. a `null` body). Thrown by {@link OllamaProvider} at its two HTTP
39
- * failure sites — the non-OK status branch and the null-body branch — so a
40
- * caller can branch on `error.status` instead of parsing the message. Narrow
41
- * a caught value with {@link isOllamaHTTPError}.
41
+ * Carries the machine-readable `code` `'HTTP'` and the response `status` (0 when no
42
+ * HTTP response was received at all, for example a `null` body). Thrown by
43
+ * {@link OllamaProvider} at its HTTP failure sites — the non-OK status branch and the
44
+ * null-body branch — so a caller can branch on `error.code` and read `error.status`
45
+ * for the HTTP number instead of parsing the message. Narrow a caught value with
46
+ * {@link isOllamaHTTPError}.
42
47
  *
43
48
  * @example
44
49
  * ```ts
@@ -52,6 +57,11 @@ var MAX_ERROR_BODY_LENGTH = 2048;
52
57
  * ```
53
58
  */
54
59
  var OllamaHTTPError = class extends Error {
60
+ /**
61
+ * Names the machine-readable condition this error reports — `'HTTP'`: an `/api/chat`
62
+ * transport, status, or body failure.
63
+ */
64
+ code = "HTTP";
55
65
  status;
56
66
  constructor(message, status, options) {
57
67
  super(message, options);
@@ -60,36 +70,252 @@ var OllamaHTTPError = class extends Error {
60
70
  }
61
71
  };
62
72
  /**
63
- * Whether a value is an {@link OllamaHTTPError}.
73
+ * Checks whether a value is an {@link OllamaHTTPError}.
64
74
  *
65
75
  * @param value - The value to test
66
- * @returns `true` when `value` is an `OllamaHTTPError`
76
+ * @returns True if `value` is an `OllamaHTTPError`; false otherwise
67
77
  */
68
78
  function isOllamaHTTPError(value) {
69
79
  return value instanceof OllamaHTTPError;
70
80
  }
71
81
  //#endregion
82
+ //#region src/server/helpers.ts
83
+ /**
84
+ * Maps conversation turns onto the `/api/chat` wire's minimal message shape.
85
+ *
86
+ * @remarks
87
+ * `tool_calls` is emitted only on a turn that replays them and `images` only on a
88
+ * multimodal turn, so an empty optional never reaches the wire.
89
+ *
90
+ * @param messages - The conversation turns to send
91
+ * @returns The wire `messages` array, one entry per turn, in order
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * mapMessages([{ id: '1', role: 'user', content: 'Say hello.' }])
96
+ * // [{ role: 'user', content: 'Say hello.' }]
97
+ * ```
98
+ */
99
+ function mapMessages(messages) {
100
+ return messages.map((message) => ({
101
+ role: message.role,
102
+ content: message.content,
103
+ ...message.calls !== void 0 && message.calls.length > 0 ? { tool_calls: message.calls.map((call) => ({ function: {
104
+ name: call.name,
105
+ arguments: call.arguments
106
+ } })) } : {},
107
+ ...message.images !== void 0 && message.images.length > 0 ? { images: [...message.images] } : {}
108
+ }));
109
+ }
110
+ /**
111
+ * Builds a provider result from a turn's content, reasoning, tool calls, and usage.
112
+ *
113
+ * @remarks
114
+ * Only the present optionals are set: no empty `thinking`, no empty `tools`, and no
115
+ * `usage` unless the wire reported one.
116
+ *
117
+ * @param content - The clean assistant content the splitter accumulated
118
+ * @param thinking - The joined reasoning, empty when the turn produced none
119
+ * @param tools - The tool calls collected across the turn
120
+ * @param usage - The token usage, or `undefined` when the wire reported none
121
+ * @returns The result carrying only its populated fields
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * buildResult('ok', '', [], undefined) // { content: 'ok' }
126
+ * ```
127
+ */
128
+ function buildResult(content, thinking, tools, usage) {
129
+ const result = { content };
130
+ if (thinking.length > 0) result.thinking = thinking;
131
+ if (tools.length > 0) result.tools = tools;
132
+ if (usage !== void 0) result.usage = usage;
133
+ return result;
134
+ }
135
+ /**
136
+ * Extracts the assistant text of one wire record.
137
+ *
138
+ * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line
139
+ * @returns The record's `message.content` when it is a string, else `''`
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * extractContent({ message: { content: 'ok' } }) // 'ok'
144
+ * ```
145
+ */
146
+ function extractContent(record) {
147
+ const message = Reflect.get(record, "message");
148
+ if (!(0, _orkestrel_contract.isRecord)(message)) return "";
149
+ const content = Reflect.get(message, "content");
150
+ return (0, _orkestrel_contract.isString)(content) ? content : "";
151
+ }
152
+ /**
153
+ * Extracts the daemon-side reasoning of one wire record.
154
+ *
155
+ * @remarks
156
+ * `message.thinking` is the `think: true` wire shape. It is read whatever the configured
157
+ * flag says, because a daemon may separate reasoning on its own.
158
+ *
159
+ * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line
160
+ * @returns The record's `message.thinking` when it is a string, else `''`
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * extractThinking({ message: { thinking: 'weighing it' } }) // 'weighing it'
165
+ * ```
166
+ */
167
+ function extractThinking(record) {
168
+ const message = Reflect.get(record, "message");
169
+ if (!(0, _orkestrel_contract.isRecord)(message)) return "";
170
+ const thinking = Reflect.get(message, "thinking");
171
+ return (0, _orkestrel_contract.isString)(thinking) ? thinking : "";
172
+ }
173
+ /**
174
+ * Joins a call's two reasoning carriers into the result's `thinking`.
175
+ *
176
+ * @param splitter - The per-call splitter holding the separated in-content spans
177
+ * @param wired - The accumulated wire-side `message.thinking` text
178
+ * @returns The two carriers separated by a blank line, or whichever one is non-empty
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * joinThinking(createThinkSplitter(), 'from the wire') // 'from the wire'
183
+ * ```
184
+ */
185
+ function joinThinking(splitter, wired) {
186
+ if (splitter.thinking.length === 0) return wired;
187
+ if (wired.length === 0) return splitter.thinking;
188
+ return `${splitter.thinking}\n\n${wired}`;
189
+ }
190
+ /**
191
+ * Extracts the token usage of one wire record.
192
+ *
193
+ * @remarks
194
+ * Both counts must be numbers, which is true of the non-stream body and the stream's
195
+ * `done: true` line. A delta line carries neither, so it yields `undefined`.
196
+ *
197
+ * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line
198
+ * @returns The `TokenUsage` shape, or `undefined` when either count is absent
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * extractUsage({ prompt_eval_count: 3, eval_count: 4 })
203
+ * // { prompt: 3, completion: 4, total: 7 }
204
+ * ```
205
+ */
206
+ function extractUsage(record) {
207
+ const prompt = Reflect.get(record, "prompt_eval_count");
208
+ const completion = Reflect.get(record, "eval_count");
209
+ if (!(0, _orkestrel_contract.isNumber)(prompt) || !(0, _orkestrel_contract.isNumber)(completion)) return void 0;
210
+ return {
211
+ prompt,
212
+ completion,
213
+ total: prompt + completion
214
+ };
215
+ }
216
+ /**
217
+ * Extracts the tool calls of one wire record's `message.tool_calls`.
218
+ *
219
+ * @remarks
220
+ * Each entry narrows to `{ id, name, arguments }`: the entry and its `function` must be
221
+ * records and `name` a string, else the entry is dropped. An id is minted when the wire
222
+ * omits one.
223
+ *
224
+ * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line
225
+ * @returns The narrowed tool calls, empty when the record carries none
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * extractTools({ message: { tool_calls: [{ function: { name: 'weather' } }] } })
230
+ * // [{ id: '…', name: 'weather', arguments: {} }]
231
+ * ```
232
+ */
233
+ function extractTools(record) {
234
+ const message = Reflect.get(record, "message");
235
+ if (!(0, _orkestrel_contract.isRecord)(message)) return [];
236
+ const calls = Reflect.get(message, "tool_calls");
237
+ if (!Array.isArray(calls)) return [];
238
+ const out = [];
239
+ for (const entry of calls) {
240
+ if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
241
+ const callable = Reflect.get(entry, "function");
242
+ if (!(0, _orkestrel_contract.isRecord)(callable)) continue;
243
+ const name = Reflect.get(callable, "name");
244
+ if (!(0, _orkestrel_contract.isString)(name)) continue;
245
+ const id = Reflect.get(entry, "id");
246
+ out.push({
247
+ id: (0, _orkestrel_contract.isString)(id) ? id : crypto.randomUUID(),
248
+ name,
249
+ arguments: extractArguments(Reflect.get(callable, "arguments"))
250
+ });
251
+ }
252
+ return out;
253
+ }
254
+ /**
255
+ * Extracts a wire `arguments` value as a record.
256
+ *
257
+ * @remarks
258
+ * Total: an object passes through, a JSON string is parsed when it yields a record, and
259
+ * a malformed string yields `{}` rather than throwing.
260
+ *
261
+ * @param value - The wire's `function.arguments` value, of unknown shape
262
+ * @returns The argument record, or `{}` when the value carries none
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * extractArguments('{"city":"Oslo"}') // { city: 'Oslo' }
267
+ * ```
268
+ */
269
+ function extractArguments(value) {
270
+ if ((0, _orkestrel_contract.isRecord)(value)) return value;
271
+ if ((0, _orkestrel_contract.isString)(value)) return (0, _orkestrel_contract.parseJSONAs)(value, _orkestrel_contract.isRecord) ?? {};
272
+ return {};
273
+ }
274
+ //#endregion
275
+ //#region src/server/parsers.ts
276
+ /**
277
+ * Parses a non-stream `/api/chat` response body into a wire record.
278
+ *
279
+ * @remarks
280
+ * Total by construction: an empty body, a body that is not JSON, and a body whose JSON is
281
+ * not an object all yield `undefined`, so a malformed daemon response never escapes as a
282
+ * `SyntaxError`. The call site supplies the empty-record default that reads as empty
283
+ * content and no usage.
284
+ *
285
+ * @param response - The 200-OK `/api/chat` response whose body is read as text
286
+ * @returns The parsed record, or `undefined` when the body is empty or malformed
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * await parseBody(new Response('{"message":{"content":"ok"}}'))
291
+ * // { message: { content: 'ok' } }
292
+ * ```
293
+ */
294
+ async function parseBody(response) {
295
+ return (0, _orkestrel_contract.parseJSONAs)(await response.text(), _orkestrel_contract.isRecord);
296
+ }
297
+ //#endregion
72
298
  //#region src/server/OllamaProvider.ts
73
299
  /**
74
- * The local Ollama inference boundary — a {@link ProviderInterface} over Ollama's
300
+ * Implements the local Ollama inference boundary — a {@link ProviderInterface} over Ollama's
75
301
  * `POST /api/chat`, both non-streaming (`generate`) and streaming NDJSON (`stream`).
76
302
  *
77
303
  * @remarks
78
304
  * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus
79
305
  * passthrough sampling `options` and mapped function `tools`. The `think` flag is
80
- * CONFIGURABLE via {@link OllamaOptions.think} (default `false`). Non-stream parses
306
+ * CONFIGURABLE through {@link OllamaOptions.think} (default `false`). Non-stream parses
81
307
  * one JSON body; stream consumes NDJSON (one JSON object per `\n`-terminated line) —
82
308
  * deltas carry `message.content`, the final `done: true` line carries the token usage.
83
- * - **Think separation (H4).** The wire `think` flag is configurable
309
+ * - **Think separation.** The wire `think` flag is configurable
84
310
  * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's
85
311
  * daemon separates reasoning NATIVELY — returning it on the distinct `message.thinking`
86
- * channel (read here via `#thinking`) instead of inline in `message.content`. EITHER
312
+ * channel (read here through `extractThinking`) instead of inline in `message.content`. EITHER
87
313
  * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon
88
314
  * may ignore `think: false` for a thinking model and inline `<think>` tags, so every
89
315
  * content delta routes through the splitter, only CLEAN content is yielded / assembled,
90
316
  * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on
91
317
  * `ProviderResult.thinking`, never in the conversation.
92
- * - **Boundary narrowing (§14).** Every wire value arrives as `unknown` and is
318
+ * - **Boundary narrowing.** Every wire value arrives as `unknown` and is
93
319
  * narrowed through guards (`isRecord` / `isString` / `isNumber`) — never `as`. A
94
320
  * missing / malformed field degrades to a sensible default (empty content, no
95
321
  * usage, `{}` arguments), never a throw.
@@ -106,7 +332,8 @@ function isOllamaHTTPError(value) {
106
332
  * `globalThis.fetch`) and {@link OllamaOptions.headers} is a per-request, possibly
107
333
  * async header injector merged over the base `Content-Type` — so a browser runtime
108
334
  * can route through the developer's own server with an obfuscated bearer token,
109
- * without this library ever handling a real API key. Both omitted ⇒ today's behaviour.
335
+ * without this library ever handling a real API key. Both omitted ⇒ the global `fetch`
336
+ * and only a JSON content type.
110
337
  * Orthogonal to the deadline: the hook is awaited inside `#fetch`'s try, so a hook
111
338
  * rejection clears the armed timer like any other request failure.
112
339
  *
@@ -117,8 +344,8 @@ function isOllamaHTTPError(value) {
117
344
  * ```
118
345
  */
119
346
  var OllamaProvider = class {
120
- id = crypto.randomUUID();
121
347
  name = "ollama";
348
+ #id;
122
349
  #model;
123
350
  #url;
124
351
  #keepAlive;
@@ -129,6 +356,7 @@ var OllamaProvider = class {
129
356
  #headers;
130
357
  #format;
131
358
  constructor(options) {
359
+ this.#id = crypto.randomUUID();
132
360
  this.#model = options.model;
133
361
  this.#url = options.url ?? "http://localhost:11434";
134
362
  this.#keepAlive = options.keepAlive ?? "5m";
@@ -140,7 +368,17 @@ var OllamaProvider = class {
140
368
  this.#format = options.format;
141
369
  }
142
370
  /**
143
- * The provider's context-framing default — the PROVIDER-DEFAULT level of
371
+ * Exposes this instance's identity — a fresh `crypto.randomUUID()` minted at
372
+ * construction, satisfying the {@link ProviderInterface.id} contract member. A second
373
+ * provider built from identical options carries a distinct id.
374
+ *
375
+ * @returns The instance's minted identifier
376
+ */
377
+ get id() {
378
+ return this.#id;
379
+ }
380
+ /**
381
+ * Exposes the provider's context-framing default — the PROVIDER-DEFAULT level of
144
382
  * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it BEATS
145
383
  * the managers' built-in framing, is BEATEN by a manager-options or per-item override).
146
384
  * Satisfies the OPTIONAL {@link ProviderInterface.format} contract member: `undefined`
@@ -154,7 +392,7 @@ var OllamaProvider = class {
154
392
  * structured-output `format` wire parameter — that one IS sent in `#body`, but only when
155
393
  * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.
156
394
  *
157
- * @returns The configured {@link ContextFormatInterface}, or `undefined` when none
395
+ * @returns The configured {@link ContextFormat}, or `undefined` when none
158
396
  */
159
397
  get format() {
160
398
  return this.#format;
@@ -162,12 +400,12 @@ var OllamaProvider = class {
162
400
  async generate(messages, signal, tools, options) {
163
401
  const { response, timeout } = await this.#fetch(messages, false, signal, tools, options);
164
402
  try {
165
- const record = await this.#parseBody(response);
403
+ const record = await parseBody(response) ?? {};
166
404
  const splitter = (0, _orkestrel_agent.createThinkSplitter)();
167
- splitter.split(this.#content(record));
405
+ splitter.split(extractContent(record));
168
406
  splitter.flush();
169
- const thinking = this.#thought(splitter, this.#thinking(record));
170
- return this.#result(splitter.content, thinking, this.#tools(record), this.#usage(record));
407
+ const thinking = joinThinking(splitter, extractThinking(record));
408
+ return buildResult(splitter.content, thinking, extractTools(record), extractUsage(record));
171
409
  } finally {
172
410
  timeout.clear();
173
411
  }
@@ -183,54 +421,63 @@ var OllamaProvider = class {
183
421
  const decoder = new TextDecoder();
184
422
  const parser = (0, _orkestrel_ndjson.createNDJSONParser)();
185
423
  const splitter = (0, _orkestrel_agent.createThinkSplitter)();
186
- const state = {
187
- splitter,
188
- wired: "",
189
- calls: [],
190
- usage: void 0
191
- };
424
+ let wired = "";
425
+ const calls = [];
426
+ let usage;
192
427
  try {
193
428
  for (;;) {
194
429
  const { value, done } = await reader.read();
195
430
  if (done) break;
196
- for (const record of parser.parse(decoder.decode(value, { stream: true }))) yield* this.#deltas(record, state);
431
+ for (const record of parser.parse(decoder.decode(value, { stream: true }))) {
432
+ const increment = yield* this.#deltas(record, splitter, usage);
433
+ wired += increment.thinking;
434
+ calls.push(...increment.calls);
435
+ usage = increment.usage;
436
+ }
197
437
  }
198
438
  const decoderTail = decoder.decode();
199
- for (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\n` : "\n")) yield* this.#deltas(record, state);
439
+ for (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\n` : "\n")) {
440
+ const increment = yield* this.#deltas(record, splitter, usage);
441
+ wired += increment.thinking;
442
+ calls.push(...increment.calls);
443
+ usage = increment.usage;
444
+ }
200
445
  const tail = splitter.flush();
201
446
  if (tail.length > 0) yield {
202
- type: "content",
447
+ channel: "content",
203
448
  text: tail
204
449
  };
205
450
  } catch (error) {
206
451
  if (combined.aborted) {
207
452
  splitter.flush();
208
- throw new _orkestrel_agent.ProviderAbortError(this.#result(splitter.content, this.#thought(splitter, state.wired), state.calls, state.usage));
453
+ throw new _orkestrel_agent.ProviderAbortError(buildResult(splitter.content, joinThinking(splitter, wired), calls, usage));
209
454
  }
210
455
  throw error;
211
456
  } finally {
212
457
  try {
213
458
  await reader.cancel();
214
459
  } catch {}
215
- parser.reset();
460
+ parser.clear();
216
461
  timeout.clear();
217
462
  }
218
- return this.#result(splitter.content, this.#thought(splitter, state.wired), state.calls, state.usage);
463
+ return buildResult(splitter.content, joinThinking(splitter, wired), calls, usage);
219
464
  }
220
- *#deltas(record, state) {
221
- const delta = state.splitter.split(this.#content(record));
465
+ *#deltas(record, splitter, usage) {
466
+ const delta = splitter.split(extractContent(record));
222
467
  if (delta.length > 0) yield {
223
- type: "content",
468
+ channel: "content",
224
469
  text: delta
225
470
  };
226
- const thinking = this.#thinking(record);
471
+ const thinking = extractThinking(record);
227
472
  if (thinking.length > 0) yield {
228
- type: "thinking",
473
+ channel: "thinking",
229
474
  text: thinking
230
475
  };
231
- state.wired += thinking;
232
- state.calls.push(...this.#tools(record));
233
- if (Reflect.get(record, "done") === true) state.usage = this.#usage(record);
476
+ return {
477
+ thinking,
478
+ calls: extractTools(record),
479
+ usage: Reflect.get(record, "done") === true ? extractUsage(record) : usage
480
+ };
234
481
  }
235
482
  async #fetch(messages, stream, signal, tools, options) {
236
483
  const timeout = new _orkestrel_timeout.Timeout({ ms: this.#timeout });
@@ -263,16 +510,6 @@ var OllamaProvider = class {
263
510
  throw error;
264
511
  }
265
512
  }
266
- async #parseBody(response) {
267
- const text = await response.text();
268
- if (text.length === 0) return {};
269
- try {
270
- const data = JSON.parse(text);
271
- return (0, _orkestrel_contract.isRecord)(data) ? data : {};
272
- } catch {
273
- return {};
274
- }
275
- }
276
513
  async #requestHeaders() {
277
514
  const headers = { "Content-Type": "application/json" };
278
515
  if (this.#headers !== void 0) for (const [key, value] of Object.entries(await this.#headers())) headers[key] = value;
@@ -281,7 +518,7 @@ var OllamaProvider = class {
281
518
  #body(messages, stream, tools, options) {
282
519
  return {
283
520
  model: this.#model,
284
- messages: this.#plain(messages),
521
+ messages: mapMessages(messages),
285
522
  stream,
286
523
  keep_alive: this.#keepAlive,
287
524
  think: options?.think ?? this.#think,
@@ -297,94 +534,18 @@ var OllamaProvider = class {
297
534
  })) } : {}
298
535
  };
299
536
  }
300
- #plain(messages) {
301
- return messages.map((message) => ({
302
- role: message.role,
303
- content: message.content,
304
- ...message.calls !== void 0 && message.calls.length > 0 ? { tool_calls: message.calls.map((call) => ({ function: {
305
- name: call.name,
306
- arguments: call.arguments
307
- } })) } : {},
308
- ...message.images !== void 0 && message.images.length > 0 ? { images: [...message.images] } : {}
309
- }));
310
- }
311
- #result(content, thinking, tools, usage) {
312
- const result = { content };
313
- if (thinking.length > 0) result.thinking = thinking;
314
- if (tools.length > 0) result.tools = tools;
315
- if (usage !== void 0) result.usage = usage;
316
- return result;
317
- }
318
- #content(record) {
319
- const message = Reflect.get(record, "message");
320
- if (!(0, _orkestrel_contract.isRecord)(message)) return "";
321
- const content = Reflect.get(message, "content");
322
- return (0, _orkestrel_contract.isString)(content) ? content : "";
323
- }
324
- #thinking(record) {
325
- const message = Reflect.get(record, "message");
326
- if (!(0, _orkestrel_contract.isRecord)(message)) return "";
327
- const thinking = Reflect.get(message, "thinking");
328
- return (0, _orkestrel_contract.isString)(thinking) ? thinking : "";
329
- }
330
- #thought(splitter, wired) {
331
- if (splitter.thinking.length === 0) return wired;
332
- if (wired.length === 0) return splitter.thinking;
333
- return `${splitter.thinking}\n\n${wired}`;
334
- }
335
- #usage(record) {
336
- const prompt = Reflect.get(record, "prompt_eval_count");
337
- const completion = Reflect.get(record, "eval_count");
338
- if (!(0, _orkestrel_contract.isNumber)(prompt) || !(0, _orkestrel_contract.isNumber)(completion)) return void 0;
339
- return {
340
- prompt,
341
- completion,
342
- total: prompt + completion
343
- };
344
- }
345
- #tools(record) {
346
- const message = Reflect.get(record, "message");
347
- if (!(0, _orkestrel_contract.isRecord)(message)) return [];
348
- const calls = Reflect.get(message, "tool_calls");
349
- if (!Array.isArray(calls)) return [];
350
- const out = [];
351
- for (const entry of calls) {
352
- if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
353
- const callable = Reflect.get(entry, "function");
354
- if (!(0, _orkestrel_contract.isRecord)(callable)) continue;
355
- const name = Reflect.get(callable, "name");
356
- if (!(0, _orkestrel_contract.isString)(name)) continue;
357
- const id = Reflect.get(entry, "id");
358
- out.push({
359
- id: (0, _orkestrel_contract.isString)(id) ? id : crypto.randomUUID(),
360
- name,
361
- arguments: this.#arguments(Reflect.get(callable, "arguments"))
362
- });
363
- }
364
- return out;
365
- }
366
- #arguments(value) {
367
- if ((0, _orkestrel_contract.isRecord)(value)) return value;
368
- if ((0, _orkestrel_contract.isString)(value)) try {
369
- const parsed = JSON.parse(value);
370
- if ((0, _orkestrel_contract.isRecord)(parsed)) return parsed;
371
- } catch {
372
- return {};
373
- }
374
- return {};
375
- }
376
537
  };
377
538
  //#endregion
378
539
  //#region src/server/factories.ts
379
540
  /**
380
- * Create a local Ollama inference provider — a {@link ProviderInterface} over the
541
+ * Creates a local Ollama inference provider — a {@link ProviderInterface} over the
381
542
  * daemon's `POST /api/chat`, supporting non-streaming `generate` and streaming
382
543
  * `stream`.
383
544
  *
384
545
  * @remarks
385
546
  * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,
386
547
  * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling
387
- * parameters (`temperature` / `seed` / `num_predict` / …). Both calls take an
548
+ * parameters (`temperature`, `seed`, and `num_predict`). Both calls take an
388
549
  * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a
389
550
  * `ProviderAbortError` carrying the partial result.
390
551
  *
@@ -392,12 +553,12 @@ var OllamaProvider = class {
392
553
  * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a
393
554
  * generated/obfuscated bearer token your server validates — so a browser runtime
394
555
  * reaches the LLM through your middleware WITHOUT this library ever handling the real API
395
- * key. Both omitted ⇒ today's behaviour (the global `fetch`, only a JSON content type).
556
+ * key. Both omitted ⇒ the global `fetch` and only a JSON content type.
396
557
  *
397
558
  * The optional `format` is the provider's context-framing default — the PROVIDER-DEFAULT
398
- * level of `AgentContext`'s format cascade (see [agents.md]; beaten by a manager-options
399
- * or per-item override, beating the managers' built-in framing), declaring how this
400
- * provider's models prefer context sections framed (e.g. XML group wrappers vs. Markdown
559
+ * level of `AgentContext`'s format cascade (beaten by a manager-options or per-item
560
+ * override, beating the managers' built-in framing), declaring how this
561
+ * provider's models prefer context sections framed (for example XML group wrappers vs. Markdown
401
562
  * headers). It is EXPOSED on the provider for the Agent's `build()` and is NOT Ollama's
402
563
  * `/api/chat` `format` wire parameter (structured output) — the two are unrelated despite
403
564
  * the shared word. Omitted ⇒ the provider is framing-agnostic (core's built-in defaults).
@@ -409,7 +570,7 @@ var OllamaProvider = class {
409
570
  * @example
410
571
  * ```ts
411
572
  * import { createAbort } from '@orkestrel/abort'
412
- * import { createOllama } from '@src/server'
573
+ * import { createOllama } from '@orkestrel/ollama'
413
574
  *
414
575
  * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })
415
576
  * const abort = createAbort()
@@ -417,7 +578,7 @@ var OllamaProvider = class {
417
578
  * ```
418
579
  *
419
580
  * @example
420
- * Route through your own server with an obfuscated token (deployment scenario S2):
581
+ * Route through your own server with an obfuscated token:
421
582
  * ```ts
422
583
  * const provider = createOllama({
423
584
  * model: 'qwen3.5:2b-q4_K_M',
@@ -453,7 +614,16 @@ exports.DEFAULT_PROVIDER_TIMEOUT = DEFAULT_PROVIDER_TIMEOUT;
453
614
  exports.MAX_ERROR_BODY_LENGTH = MAX_ERROR_BODY_LENGTH;
454
615
  exports.OllamaHTTPError = OllamaHTTPError;
455
616
  exports.OllamaProvider = OllamaProvider;
617
+ exports.buildResult = buildResult;
456
618
  exports.createOllama = createOllama;
619
+ exports.extractArguments = extractArguments;
620
+ exports.extractContent = extractContent;
621
+ exports.extractThinking = extractThinking;
622
+ exports.extractTools = extractTools;
623
+ exports.extractUsage = extractUsage;
457
624
  exports.isOllamaHTTPError = isOllamaHTTPError;
625
+ exports.joinThinking = joinThinking;
626
+ exports.mapMessages = mapMessages;
627
+ exports.parseBody = parseBody;
458
628
 
459
629
  //# sourceMappingURL=index.cjs.map