@orkestrel/ollama 0.0.13 → 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.
- package/README.md +6 -5
- package/dist/src/server/index.cjs +312 -142
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +261 -65
- package/dist/src/server/index.d.ts +261 -65
- package/dist/src/server/index.js +304 -143
- package/dist/src/server/index.js.map +1 -1
- package/package.json +19 -20
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
38
|
-
*
|
|
39
|
-
* failure sites — the non-OK status branch and the
|
|
40
|
-
* caller can branch on `error.
|
|
41
|
-
* a caught value with
|
|
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
|
-
*
|
|
73
|
+
* Checks whether a value is an {@link OllamaHTTPError}.
|
|
64
74
|
*
|
|
65
75
|
* @param value - The value to test
|
|
66
|
-
* @returns
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ⇒
|
|
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
|
-
*
|
|
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
|
|
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
|
|
403
|
+
const record = await parseBody(response) ?? {};
|
|
166
404
|
const splitter = (0, _orkestrel_agent.createThinkSplitter)();
|
|
167
|
-
splitter.split(
|
|
405
|
+
splitter.split(extractContent(record));
|
|
168
406
|
splitter.flush();
|
|
169
|
-
const thinking =
|
|
170
|
-
return
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 })))
|
|
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"))
|
|
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
|
-
|
|
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(
|
|
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.
|
|
460
|
+
parser.clear();
|
|
216
461
|
timeout.clear();
|
|
217
462
|
}
|
|
218
|
-
return
|
|
463
|
+
return buildResult(splitter.content, joinThinking(splitter, wired), calls, usage);
|
|
219
464
|
}
|
|
220
|
-
*#deltas(record,
|
|
221
|
-
const delta =
|
|
465
|
+
*#deltas(record, splitter, usage) {
|
|
466
|
+
const delta = splitter.split(extractContent(record));
|
|
222
467
|
if (delta.length > 0) yield {
|
|
223
|
-
|
|
468
|
+
channel: "content",
|
|
224
469
|
text: delta
|
|
225
470
|
};
|
|
226
|
-
const thinking =
|
|
471
|
+
const thinking = extractThinking(record);
|
|
227
472
|
if (thinking.length > 0) yield {
|
|
228
|
-
|
|
473
|
+
channel: "thinking",
|
|
229
474
|
text: thinking
|
|
230
475
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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 ⇒
|
|
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 (
|
|
399
|
-
*
|
|
400
|
-
* provider's models prefer context sections framed (
|
|
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 '@
|
|
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
|
|
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
|