@orkestrel/ollama 0.0.14 → 0.0.16
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 +32 -14
- package/dist/src/core/index.cjs +362 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +351 -0
- package/dist/src/core/index.d.ts +351 -0
- package/dist/src/core/index.js +351 -0
- package/dist/src/core/index.js.map +1 -0
- package/package.json +35 -36
- package/dist/src/server/index.cjs +0 -629
- package/dist/src/server/index.cjs.map +0 -1
- package/dist/src/server/index.d.cts +0 -542
- package/dist/src/server/index.d.ts +0 -542
- package/dist/src/server/index.js +0 -612
- package/dist/src/server/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# @orkestrel/ollama
|
|
2
2
|
|
|
3
|
-
A typed local-LLM provider for the `@orkestrel` line
|
|
4
|
-
|
|
5
|
-
streaming, tool calls, thinking, and usage accounting
|
|
6
|
-
|
|
3
|
+
> A typed local-LLM provider for the `@orkestrel` line: the Ollama daemon's `POST /api/chat`
|
|
4
|
+
> wire carried on the shared `AgentProvider` engine from `@orkestrel/agent`, with NDJSON
|
|
5
|
+
> streaming, tool calls, thinking, and usage accounting narrowed off the wire through
|
|
6
|
+
> `@orkestrel/contract` guards and no Ollama SDK dependency.
|
|
7
|
+
|
|
8
|
+
Create a provider with the `createOllama` function, hand it a conversation and a
|
|
9
|
+
bounding `AbortSignal`, and read the assembled `ProviderResult` the `generate`
|
|
10
|
+
method resolves — or drive the `stream` method for live deltas. The engine in
|
|
11
|
+
`@orkestrel/agent` holds the deadline, the transport, the header hook, and the
|
|
12
|
+
result assembly; this package holds the Ollama wire. Point `url` at your own server
|
|
13
|
+
and attach a short-lived token through `headers` where a browser runtime must not
|
|
14
|
+
hold the real key, or relay the whole provider through your server so the page never
|
|
15
|
+
learns which model answers.
|
|
7
16
|
|
|
8
17
|
## Install
|
|
9
18
|
|
|
@@ -13,10 +22,10 @@ npm install @orkestrel/ollama
|
|
|
13
22
|
|
|
14
23
|
## Requirements
|
|
15
24
|
|
|
16
|
-
- Node.js >= 22
|
|
17
|
-
- A
|
|
25
|
+
- Node.js >= 22, or any browser with `fetch` and `ReadableStream`
|
|
26
|
+
- A reachable Ollama daemon (default `http://localhost:11434`) with a pulled
|
|
18
27
|
model — required at runtime by any consumer, and by this repository's live
|
|
19
|
-
`service` test project; the `src:
|
|
28
|
+
`service` test project; the `src:core` project is hermetic and passes with
|
|
20
29
|
the daemon down
|
|
21
30
|
- ESM + CJS (dual-format build)
|
|
22
31
|
|
|
@@ -41,27 +50,36 @@ output, `stream` is the same call streamed — it yields `ProviderDelta`s
|
|
|
41
50
|
assembled result when the stream completes:
|
|
42
51
|
|
|
43
52
|
```ts
|
|
53
|
+
const answer: string[] = []
|
|
44
54
|
const generator = provider.stream(messages, abort.signal)
|
|
45
55
|
let step = await generator.next()
|
|
46
56
|
while (!step.done) {
|
|
47
|
-
if (step.value.channel === 'content')
|
|
57
|
+
if (step.value.channel === 'content') answer.push(step.value.text)
|
|
48
58
|
step = await generator.next()
|
|
49
59
|
}
|
|
50
60
|
const streamed = step.value // the assembled ProviderResult
|
|
61
|
+
streamed.content // the answer — the settled content is the authoritative one
|
|
62
|
+
answer.join('') // what arrived on the content channel
|
|
51
63
|
```
|
|
52
64
|
|
|
65
|
+
Read the answer from the settled `streamed.content`. The deltas are what arrived,
|
|
66
|
+
and a turn whose reasoning the daemon opened without a `<think>` marker streams a
|
|
67
|
+
prefix the base later moves to `streamed.thinking`, leaving the joined deltas
|
|
68
|
+
longer than the settled content — see [`guides/ollama.md`](guides/ollama.md).
|
|
69
|
+
|
|
53
70
|
## Guide
|
|
54
71
|
|
|
55
|
-
For the full surface — `createOllama`, `OllamaProvider`, `OllamaOptions`,
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
For the full surface — `createOllama`, `OllamaProvider`, `OllamaOptions`, the
|
|
73
|
+
wire seams (`frame` / `body` / `read` / `finish`), tool calls
|
|
74
|
+
(`ToolDefinition` / `ToolCall` from `@orkestrel/tool`), thinking, the
|
|
75
|
+
context-framing default, and the browser relay — see
|
|
58
76
|
[`guides/ollama.md`](guides/ollama.md).
|
|
59
77
|
|
|
60
78
|
## Package
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
The `exports` field in `package.json` publishes the `.` entry alone — a dual
|
|
81
|
+
ESM + CommonJS build of `src/core`, host-independent so the same build serves a
|
|
82
|
+
server process and a browser page.
|
|
65
83
|
|
|
66
84
|
## License
|
|
67
85
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
|
+
let _orkestrel_agent = require("@orkestrel/agent");
|
|
4
|
+
let _orkestrel_ndjson = require("@orkestrel/ndjson");
|
|
5
|
+
//#region src/core/constants.ts
|
|
6
|
+
/**
|
|
7
|
+
* Names the local Ollama daemon base URL, `'http://localhost:11434'`, assumed when
|
|
8
|
+
* `OllamaOptions.url` is omitted.
|
|
9
|
+
*/
|
|
10
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
11
|
+
/**
|
|
12
|
+
* Names how long the model stays resident after a call — `'5m'` when
|
|
13
|
+
* `OllamaOptions.keepAlive` is omitted, Ollama's own `keep_alive` default, expressed as a
|
|
14
|
+
* duration string.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so
|
|
18
|
+
* the constant, the `OllamaOptions.keepAlive` key, and the wire member read as one term.
|
|
19
|
+
*/
|
|
20
|
+
var DEFAULT_KEEP_ALIVE = "5m";
|
|
21
|
+
/** Names the Ollama chat endpoint appended to the configured base URL. */
|
|
22
|
+
var OLLAMA_CHAT_PATH = "/api/chat";
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/core/helpers.ts
|
|
25
|
+
/**
|
|
26
|
+
* Maps conversation turns onto the `/api/chat` wire's minimal message shape.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* `tool_calls` is emitted only on a turn that replays them and `images` only on a
|
|
30
|
+
* multimodal turn, so an empty optional never reaches the wire.
|
|
31
|
+
*
|
|
32
|
+
* @param messages - The conversation turns to send
|
|
33
|
+
* @returns The wire `messages` array, one entry per turn, in order
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* mapMessages([{ id: '1', role: 'user', content: 'Say hello.' }])
|
|
38
|
+
* // [{ role: 'user', content: 'Say hello.' }]
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
function mapMessages(messages) {
|
|
42
|
+
return messages.map((message) => ({
|
|
43
|
+
role: message.role,
|
|
44
|
+
content: message.content,
|
|
45
|
+
...message.calls !== void 0 && message.calls.length > 0 ? { tool_calls: message.calls.map((call) => ({ function: {
|
|
46
|
+
name: call.name,
|
|
47
|
+
arguments: call.arguments
|
|
48
|
+
} })) } : {},
|
|
49
|
+
...message.images !== void 0 && message.images.length > 0 ? { images: [...message.images] } : {}
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the assistant text of one wire record.
|
|
54
|
+
*
|
|
55
|
+
* @param record - One parsed `/api/chat` NDJSON record
|
|
56
|
+
* @returns The record's `message.content` when it is a string, else `''`
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* extractContent({ message: { content: 'ok' } }) // 'ok'
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function extractContent(record) {
|
|
64
|
+
const message = Reflect.get(record, "message");
|
|
65
|
+
if (!(0, _orkestrel_contract.isRecord)(message)) return "";
|
|
66
|
+
const content = Reflect.get(message, "content");
|
|
67
|
+
return (0, _orkestrel_contract.isString)(content) ? content : "";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extracts the daemon-side reasoning of one wire record.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* `message.thinking` is the `think: true` wire shape. It is read whatever the configured
|
|
74
|
+
* flag says, because a daemon may separate reasoning on its own.
|
|
75
|
+
*
|
|
76
|
+
* @param record - One parsed `/api/chat` NDJSON record
|
|
77
|
+
* @returns The record's `message.thinking` when it is a string, else `''`
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* extractThinking({ message: { thinking: 'weighing it' } }) // 'weighing it'
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
function extractThinking(record) {
|
|
85
|
+
const message = Reflect.get(record, "message");
|
|
86
|
+
if (!(0, _orkestrel_contract.isRecord)(message)) return "";
|
|
87
|
+
const thinking = Reflect.get(message, "thinking");
|
|
88
|
+
return (0, _orkestrel_contract.isString)(thinking) ? thinking : "";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extracts the token usage of one wire record.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* Both counts must be numbers, which is true of the stream's `done: true` line. A
|
|
95
|
+
* delta line carries neither, so it yields `undefined`.
|
|
96
|
+
*
|
|
97
|
+
* @param record - One parsed `/api/chat` NDJSON record
|
|
98
|
+
* @returns The `TokenUsage` shape, or `undefined` when either count is absent
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* extractUsage({ prompt_eval_count: 3, eval_count: 4 })
|
|
103
|
+
* // { prompt: 3, completion: 4, total: 7 }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function extractUsage(record) {
|
|
107
|
+
const prompt = Reflect.get(record, "prompt_eval_count");
|
|
108
|
+
const completion = Reflect.get(record, "eval_count");
|
|
109
|
+
if (!(0, _orkestrel_contract.isNumber)(prompt) || !(0, _orkestrel_contract.isNumber)(completion)) return void 0;
|
|
110
|
+
return {
|
|
111
|
+
prompt,
|
|
112
|
+
completion,
|
|
113
|
+
total: prompt + completion
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extracts the tool calls of one wire record's `message.tool_calls`.
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Each entry narrows to `{ id, name, arguments }`: the entry and its `function` must be
|
|
121
|
+
* records and `name` a string, else the entry is dropped. An id is minted when the wire
|
|
122
|
+
* omits one.
|
|
123
|
+
*
|
|
124
|
+
* @param record - One parsed `/api/chat` NDJSON record
|
|
125
|
+
* @returns The narrowed tool calls, empty when the record carries none
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* extractTools({ message: { tool_calls: [{ function: { name: 'weather' } }] } })
|
|
130
|
+
* // [{ id: '…', name: 'weather', arguments: {} }]
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
function extractTools(record) {
|
|
134
|
+
const message = Reflect.get(record, "message");
|
|
135
|
+
if (!(0, _orkestrel_contract.isRecord)(message)) return [];
|
|
136
|
+
const calls = Reflect.get(message, "tool_calls");
|
|
137
|
+
if (!Array.isArray(calls)) return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const entry of calls) {
|
|
140
|
+
if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
|
|
141
|
+
const callable = Reflect.get(entry, "function");
|
|
142
|
+
if (!(0, _orkestrel_contract.isRecord)(callable)) continue;
|
|
143
|
+
const name = Reflect.get(callable, "name");
|
|
144
|
+
if (!(0, _orkestrel_contract.isString)(name)) continue;
|
|
145
|
+
const id = Reflect.get(entry, "id");
|
|
146
|
+
out.push({
|
|
147
|
+
id: (0, _orkestrel_contract.isString)(id) ? id : crypto.randomUUID(),
|
|
148
|
+
name,
|
|
149
|
+
arguments: extractArguments(Reflect.get(callable, "arguments"))
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extracts a wire `arguments` value as a record.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* Total: an object passes through, a JSON string is parsed when it yields a record, and
|
|
159
|
+
* a malformed string yields `{}` rather than throwing.
|
|
160
|
+
*
|
|
161
|
+
* @param value - The wire's `function.arguments` value, of unknown shape
|
|
162
|
+
* @returns The argument record, or `{}` when the value carries none
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* extractArguments('{"city":"Oslo"}') // { city: 'Oslo' }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
function extractArguments(value) {
|
|
170
|
+
if ((0, _orkestrel_contract.isRecord)(value)) return value;
|
|
171
|
+
if ((0, _orkestrel_contract.isString)(value)) return (0, _orkestrel_contract.parseJSONAs)(value, _orkestrel_contract.isRecord) ?? {};
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/core/OllamaProvider.ts
|
|
176
|
+
/**
|
|
177
|
+
* Implements the Ollama `/api/chat` wire over the shared {@link AgentProvider} engine.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* Every request uses NDJSON streaming. The base assembles complete turns, separates
|
|
181
|
+
* reasoning, and bounds requests; this class supplies Ollama framing and projections.
|
|
182
|
+
* Usage comes only from a `done: true` record carrying the token counts.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })
|
|
187
|
+
* const result = await provider.generate(messages, abort.signal)
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
var OllamaProvider = class extends _orkestrel_agent.AgentProvider {
|
|
191
|
+
name = "ollama";
|
|
192
|
+
#model;
|
|
193
|
+
#keepAlive;
|
|
194
|
+
#think;
|
|
195
|
+
#options;
|
|
196
|
+
constructor(options) {
|
|
197
|
+
super({
|
|
198
|
+
url: options.url ?? "http://localhost:11434",
|
|
199
|
+
path: OLLAMA_CHAT_PATH,
|
|
200
|
+
...options.timeout === void 0 ? {} : { timeout: options.timeout },
|
|
201
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch },
|
|
202
|
+
...options.headers === void 0 ? {} : { headers: options.headers },
|
|
203
|
+
...options.format === void 0 ? {} : { format: options.format }
|
|
204
|
+
});
|
|
205
|
+
this.#model = options.model;
|
|
206
|
+
this.#keepAlive = options.keepAlive ?? "5m";
|
|
207
|
+
this.#think = options.think ?? false;
|
|
208
|
+
this.#options = options.options;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Creates fresh NDJSON framing state for a call.
|
|
212
|
+
*
|
|
213
|
+
* @returns The parser that buffers incomplete Ollama records
|
|
214
|
+
*/
|
|
215
|
+
frame() {
|
|
216
|
+
return (0, _orkestrel_ndjson.createNDJSONParser)();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Projects conversation turns and per-call options onto the Ollama request body.
|
|
220
|
+
*
|
|
221
|
+
* @param request - The conversation, advertised tools, and per-call overrides
|
|
222
|
+
* @returns The `/api/chat` body with streaming enabled
|
|
223
|
+
*/
|
|
224
|
+
body(request) {
|
|
225
|
+
return {
|
|
226
|
+
model: this.#model,
|
|
227
|
+
messages: mapMessages(request.messages),
|
|
228
|
+
stream: true,
|
|
229
|
+
keep_alive: this.#keepAlive,
|
|
230
|
+
think: request.options?.think ?? this.#think,
|
|
231
|
+
...this.#options !== void 0 ? { options: this.#options } : {},
|
|
232
|
+
...request.options?.schema !== void 0 ? { format: request.options.schema } : {},
|
|
233
|
+
...request.tools !== void 0 && request.tools.length > 0 ? { tools: request.tools.map((tool) => ({
|
|
234
|
+
type: "function",
|
|
235
|
+
function: {
|
|
236
|
+
name: tool.name,
|
|
237
|
+
...tool.description === void 0 ? {} : { description: tool.description },
|
|
238
|
+
...tool.parameters === void 0 ? {} : { parameters: tool.parameters }
|
|
239
|
+
}
|
|
240
|
+
})) } : {}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Extracts a record's content, reasoning, tools, and completed usage report.
|
|
245
|
+
*
|
|
246
|
+
* @param record - One parsed Ollama NDJSON record
|
|
247
|
+
* @returns The turn increment, omitting usage until `done` and the counts are present
|
|
248
|
+
*/
|
|
249
|
+
read(record) {
|
|
250
|
+
const usage = Reflect.get(record, "done") === true ? extractUsage(record) : void 0;
|
|
251
|
+
return {
|
|
252
|
+
content: extractContent(record),
|
|
253
|
+
thinking: extractThinking(record),
|
|
254
|
+
tools: extractTools(record),
|
|
255
|
+
...usage === void 0 ? {} : { usage }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Recovers a final NDJSON record that arrived without its line terminator.
|
|
260
|
+
*
|
|
261
|
+
* @param parser - The call's parser holding any unterminated input
|
|
262
|
+
* @returns The records completed by the final newline
|
|
263
|
+
*/
|
|
264
|
+
finish(parser) {
|
|
265
|
+
return parser.parse("\n");
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/core/factories.ts
|
|
270
|
+
/**
|
|
271
|
+
* Creates a local Ollama inference provider — a {@link ProviderInterface} over the
|
|
272
|
+
* daemon's `POST /api/chat`, assembling `generate` from the same NDJSON engine as `stream`.
|
|
273
|
+
*
|
|
274
|
+
* @remarks
|
|
275
|
+
* Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,
|
|
276
|
+
* `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling
|
|
277
|
+
* parameters (`temperature`, `seed`, and `num_predict`). Each call takes an
|
|
278
|
+
* `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a
|
|
279
|
+
* `ProviderAbortError` carrying the partial result.
|
|
280
|
+
*
|
|
281
|
+
* The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):
|
|
282
|
+
* point `url` at your own server, inject a custom `fetch`, and have `headers` attach a
|
|
283
|
+
* generated/obfuscated bearer token your server validates — so a browser runtime
|
|
284
|
+
* reaches the LLM through your middleware without this library ever handling the real API
|
|
285
|
+
* key. Both omitted ⇒ the global `fetch` and only a JSON content type.
|
|
286
|
+
*
|
|
287
|
+
* The optional `format` is the provider's context-framing default — the provider-default
|
|
288
|
+
* level of `AgentContext`'s format cascade (beaten by a manager-options or per-item
|
|
289
|
+
* override, beating the managers' built-in framing), declaring how this
|
|
290
|
+
* provider's models prefer context sections framed (for example XML group wrappers vs. Markdown
|
|
291
|
+
* headers). It is exposed on the provider for the Agent's `build()` and is not Ollama's
|
|
292
|
+
* `/api/chat` `format` wire parameter (structured output) — the framing default and that
|
|
293
|
+
* wire parameter are unrelated despite the shared word. Omitted ⇒ the provider is
|
|
294
|
+
* framing-agnostic (core's built-in defaults).
|
|
295
|
+
*
|
|
296
|
+
* @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /
|
|
297
|
+
* `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})
|
|
298
|
+
* @returns A working {@link ProviderInterface} backed by Ollama
|
|
299
|
+
*
|
|
300
|
+
* @example createOllama + generate
|
|
301
|
+
* ```ts
|
|
302
|
+
* import { createAbort } from '@orkestrel/abort'
|
|
303
|
+
* import type { TokenUsage } from '@orkestrel/budget'
|
|
304
|
+
* import { createOllama } from '@orkestrel/ollama'
|
|
305
|
+
*
|
|
306
|
+
* declare function charge(usage: TokenUsage): void // your billing integration
|
|
307
|
+
*
|
|
308
|
+
* const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', options: { temperature: 0 } })
|
|
309
|
+
* const abort = createAbort()
|
|
310
|
+
* const messages = [
|
|
311
|
+
* { id: '1', role: 'user', content: 'Summarize the release notes for version 2.0.' },
|
|
312
|
+
* ] as const
|
|
313
|
+
*
|
|
314
|
+
* const result = await provider.generate(messages, abort.signal)
|
|
315
|
+
* console.log(result.content)
|
|
316
|
+
* if (result.usage) charge(result.usage) // fold into a token budget
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* Route through your own server with an obfuscated token:
|
|
321
|
+
* ```ts
|
|
322
|
+
* const provider = createOllama({
|
|
323
|
+
* model: 'qwen3.5:2b-q4_K_M',
|
|
324
|
+
* url: 'https://my-app.example.com/llm', // your server, not the daemon
|
|
325
|
+
* fetch: myFetch, // optional custom transport
|
|
326
|
+
* headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this
|
|
327
|
+
* })
|
|
328
|
+
* ```
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* Declare a context-framing default — wrap the instructions section in an XML group (the
|
|
332
|
+
* provider-default level of `AgentContext`'s cascade; not the wire `format`):
|
|
333
|
+
* ```ts
|
|
334
|
+
* const provider = createOllama({
|
|
335
|
+
* model: 'qwen3.5:2b-q4_K_M',
|
|
336
|
+
* format: {
|
|
337
|
+
* instructions: {
|
|
338
|
+
* open: '<instructions>',
|
|
339
|
+
* render: (i) => `<instruction>${i.content}</instruction>`,
|
|
340
|
+
* close: '</instructions>',
|
|
341
|
+
* },
|
|
342
|
+
* },
|
|
343
|
+
* })
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
function createOllama(options) {
|
|
347
|
+
return new OllamaProvider(options);
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
exports.DEFAULT_KEEP_ALIVE = DEFAULT_KEEP_ALIVE;
|
|
351
|
+
exports.DEFAULT_OLLAMA_URL = DEFAULT_OLLAMA_URL;
|
|
352
|
+
exports.OLLAMA_CHAT_PATH = OLLAMA_CHAT_PATH;
|
|
353
|
+
exports.OllamaProvider = OllamaProvider;
|
|
354
|
+
exports.createOllama = createOllama;
|
|
355
|
+
exports.extractArguments = extractArguments;
|
|
356
|
+
exports.extractContent = extractContent;
|
|
357
|
+
exports.extractThinking = extractThinking;
|
|
358
|
+
exports.extractTools = extractTools;
|
|
359
|
+
exports.extractUsage = extractUsage;
|
|
360
|
+
exports.mapMessages = mapMessages;
|
|
361
|
+
|
|
362
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/core/OllamaProvider.ts","../../../src/core/factories.ts"],"sourcesContent":["// Ollama constants — the provider's defaults.\n\n/**\n * Names the local Ollama daemon base URL, `'http://localhost:11434'`, assumed when\n * `OllamaOptions.url` is omitted.\n */\nexport const DEFAULT_OLLAMA_URL = 'http://localhost:11434'\n\n/**\n * Names how long the model stays resident after a call — `'5m'` when\n * `OllamaOptions.keepAlive` is omitted, Ollama's own `keep_alive` default, expressed as a\n * duration string.\n *\n * @remarks\n * The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so\n * the constant, the `OllamaOptions.keepAlive` key, and the wire member read as one term.\n */\nexport const DEFAULT_KEEP_ALIVE = '5m'\n\n/** Names the Ollama chat endpoint appended to the configured base URL. */\nexport const OLLAMA_CHAT_PATH = '/api/chat'\n","// The Ollama wire leaves — the request projections and the response extractions\n// `OllamaProvider` composes. Each is a pure, total function of its parameters: a missing\n// or malformed wire field degrades to a sensible default (empty content, no usage, `{}`\n// arguments), never a throw, and no value is reached through `as`.\n\nimport type { Message } from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall } from '@orkestrel/tool'\nimport type { WireChatRequest } from './types.js'\nimport { isNumber, isRecord, isString, parseJSONAs } from '@orkestrel/contract'\n\n/**\n * Maps conversation turns onto the `/api/chat` wire's minimal message shape.\n *\n * @remarks\n * `tool_calls` is emitted only on a turn that replays them and `images` only on a\n * multimodal turn, so an empty optional never reaches the wire.\n *\n * @param messages - The conversation turns to send\n * @returns The wire `messages` array, one entry per turn, in order\n *\n * @example\n * ```ts\n * mapMessages([{ id: '1', role: 'user', content: 'Say hello.' }])\n * // [{ role: 'user', content: 'Say hello.' }]\n * ```\n */\nexport function mapMessages(messages: readonly Message[]): WireChatRequest['messages'] {\n\treturn messages.map((message) => ({\n\t\trole: message.role,\n\t\tcontent: message.content,\n\t\t...(message.calls !== undefined && message.calls.length > 0\n\t\t\t? {\n\t\t\t\t\ttool_calls: message.calls.map((call) => ({\n\t\t\t\t\t\tfunction: { name: call.name, arguments: call.arguments },\n\t\t\t\t\t})),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t// Forward multimodal image data — Ollama accepts a base64 `images` array on a\n\t\t// message, which a vision-capable model receives alongside the text content.\n\t\t...(message.images !== undefined && message.images.length > 0\n\t\t\t? { images: [...message.images] }\n\t\t\t: {}),\n\t}))\n}\n\n/**\n * Extracts the assistant text of one wire record.\n *\n * @param record - One parsed `/api/chat` NDJSON record\n * @returns The record's `message.content` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractContent({ message: { content: 'ok' } }) // 'ok'\n * ```\n */\nexport function extractContent(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst content = Reflect.get(message, 'content')\n\treturn isString(content) ? content : ''\n}\n\n/**\n * Extracts the daemon-side reasoning of one wire record.\n *\n * @remarks\n * `message.thinking` is the `think: true` wire shape. It is read whatever the configured\n * flag says, because a daemon may separate reasoning on its own.\n *\n * @param record - One parsed `/api/chat` NDJSON record\n * @returns The record's `message.thinking` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractThinking({ message: { thinking: 'weighing it' } }) // 'weighing it'\n * ```\n */\nexport function extractThinking(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst thinking = Reflect.get(message, 'thinking')\n\treturn isString(thinking) ? thinking : ''\n}\n\n/**\n * Extracts the token usage of one wire record.\n *\n * @remarks\n * Both counts must be numbers, which is true of the stream's `done: true` line. A\n * delta line carries neither, so it yields `undefined`.\n *\n * @param record - One parsed `/api/chat` NDJSON record\n * @returns The `TokenUsage` shape, or `undefined` when either count is absent\n *\n * @example\n * ```ts\n * extractUsage({ prompt_eval_count: 3, eval_count: 4 })\n * // { prompt: 3, completion: 4, total: 7 }\n * ```\n */\nexport function extractUsage(record: Readonly<Record<string, unknown>>): TokenUsage | undefined {\n\tconst prompt = Reflect.get(record, 'prompt_eval_count')\n\tconst completion = Reflect.get(record, 'eval_count')\n\tif (!isNumber(prompt) || !isNumber(completion)) return undefined\n\treturn { prompt, completion, total: prompt + completion }\n}\n\n/**\n * Extracts the tool calls of one wire record's `message.tool_calls`.\n *\n * @remarks\n * Each entry narrows to `{ id, name, arguments }`: the entry and its `function` must be\n * records and `name` a string, else the entry is dropped. An id is minted when the wire\n * omits one.\n *\n * @param record - One parsed `/api/chat` NDJSON record\n * @returns The narrowed tool calls, empty when the record carries none\n *\n * @example\n * ```ts\n * extractTools({ message: { tool_calls: [{ function: { name: 'weather' } }] } })\n * // [{ id: '…', name: 'weather', arguments: {} }]\n * ```\n */\nexport function extractTools(record: Readonly<Record<string, unknown>>): readonly ToolCall[] {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return []\n\tconst calls = Reflect.get(message, 'tool_calls')\n\tif (!Array.isArray(calls)) return []\n\tconst out: ToolCall[] = []\n\tfor (const entry of calls) {\n\t\tif (!isRecord(entry)) continue\n\t\tconst callable = Reflect.get(entry, 'function')\n\t\tif (!isRecord(callable)) continue\n\t\tconst name = Reflect.get(callable, 'name')\n\t\tif (!isString(name)) continue\n\t\tconst id = Reflect.get(entry, 'id')\n\t\tout.push({\n\t\t\tid: isString(id) ? id : crypto.randomUUID(),\n\t\t\tname,\n\t\t\targuments: extractArguments(Reflect.get(callable, 'arguments')),\n\t\t})\n\t}\n\treturn out\n}\n\n/**\n * Extracts a wire `arguments` value as a record.\n *\n * @remarks\n * Total: an object passes through, a JSON string is parsed when it yields a record, and\n * a malformed string yields `{}` rather than throwing.\n *\n * @param value - The wire's `function.arguments` value, of unknown shape\n * @returns The argument record, or `{}` when the value carries none\n *\n * @example\n * ```ts\n * extractArguments('{\"city\":\"Oslo\"}') // { city: 'Oslo' }\n * ```\n */\nexport function extractArguments(value: unknown): Readonly<Record<string, unknown>> {\n\tif (isRecord(value)) return value\n\tif (isString(value)) return parseJSONAs(value, isRecord) ?? {}\n\treturn {}\n}\n","import type {\n\tAgentProviderInterface,\n\tProviderIncrement,\n\tProviderParserInterface,\n\tProviderRequest,\n} from '@orkestrel/agent'\nimport type { OllamaOptions, WireChatRequest } from './types.js'\nimport { AgentProvider } from '@orkestrel/agent'\nimport { createNDJSONParser } from '@orkestrel/ndjson'\nimport { DEFAULT_KEEP_ALIVE, DEFAULT_OLLAMA_URL, OLLAMA_CHAT_PATH } from './constants.js'\nimport {\n\textractContent,\n\textractThinking,\n\textractTools,\n\textractUsage,\n\tmapMessages,\n} from './helpers.js'\n\n/**\n * Implements the Ollama `/api/chat` wire over the shared {@link AgentProvider} engine.\n *\n * @remarks\n * Every request uses NDJSON streaming. The base assembles complete turns, separates\n * reasoning, and bounds requests; this class supplies Ollama framing and projections.\n * Usage comes only from a `done: true` record carrying the token counts.\n *\n * @example\n * ```ts\n * const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })\n * const result = await provider.generate(messages, abort.signal)\n * ```\n */\nexport class OllamaProvider extends AgentProvider implements AgentProviderInterface {\n\treadonly name = 'ollama'\n\treadonly #model: string\n\treadonly #keepAlive: string | number\n\treadonly #think: boolean\n\treadonly #options: Readonly<Record<string, unknown>> | undefined\n\n\tconstructor(options: OllamaOptions) {\n\t\tsuper({\n\t\t\turl: options.url ?? DEFAULT_OLLAMA_URL,\n\t\t\tpath: OLLAMA_CHAT_PATH,\n\t\t\t...(options.timeout === undefined ? {} : { timeout: options.timeout }),\n\t\t\t...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n\t\t\t...(options.headers === undefined ? {} : { headers: options.headers }),\n\t\t\t...(options.format === undefined ? {} : { format: options.format }),\n\t\t})\n\t\tthis.#model = options.model\n\t\tthis.#keepAlive = options.keepAlive ?? DEFAULT_KEEP_ALIVE\n\t\tthis.#think = options.think ?? false\n\t\tthis.#options = options.options\n\t}\n\n\t/**\n\t * Creates fresh NDJSON framing state for a call.\n\t *\n\t * @returns The parser that buffers incomplete Ollama records\n\t */\n\tframe(): ProviderParserInterface {\n\t\treturn createNDJSONParser()\n\t}\n\n\t/**\n\t * Projects conversation turns and per-call options onto the Ollama request body.\n\t *\n\t * @param request - The conversation, advertised tools, and per-call overrides\n\t * @returns The `/api/chat` body with streaming enabled\n\t */\n\tbody(request: ProviderRequest): WireChatRequest {\n\t\treturn {\n\t\t\tmodel: this.#model,\n\t\t\tmessages: mapMessages(request.messages),\n\t\t\tstream: true,\n\t\t\tkeep_alive: this.#keepAlive,\n\t\t\tthink: request.options?.think ?? this.#think,\n\t\t\t...(this.#options !== undefined ? { options: this.#options } : {}),\n\t\t\t...(request.options?.schema !== undefined ? { format: request.options.schema } : {}),\n\t\t\t...(request.tools !== undefined && request.tools.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttools: request.tools.map((tool): NonNullable<WireChatRequest['tools']>[number] => ({\n\t\t\t\t\t\t\ttype: 'function',\n\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\t\t\t...(tool.description === undefined ? {} : { description: tool.description }),\n\t\t\t\t\t\t\t\t...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t}\n\t}\n\n\t/**\n\t * Extracts a record's content, reasoning, tools, and completed usage report.\n\t *\n\t * @param record - One parsed Ollama NDJSON record\n\t * @returns The turn increment, omitting usage until `done` and the counts are present\n\t */\n\tread(record: Readonly<Record<string, unknown>>): ProviderIncrement {\n\t\tconst usage = Reflect.get(record, 'done') === true ? extractUsage(record) : undefined\n\t\treturn {\n\t\t\tcontent: extractContent(record),\n\t\t\tthinking: extractThinking(record),\n\t\t\ttools: extractTools(record),\n\t\t\t...(usage === undefined ? {} : { usage }),\n\t\t}\n\t}\n\n\t/**\n\t * Recovers a final NDJSON record that arrived without its line terminator.\n\t *\n\t * @param parser - The call's parser holding any unterminated input\n\t * @returns The records completed by the final newline\n\t */\n\tfinish(parser: ProviderParserInterface): ReadonlyArray<Readonly<Record<string, unknown>>> {\n\t\treturn parser.parse('\\n')\n\t}\n}\n","import type { ProviderInterface } from '@orkestrel/agent'\nimport type { OllamaOptions } from './types.js'\nimport { OllamaProvider } from './OllamaProvider.js'\n\n/**\n * Creates a local Ollama inference provider — a {@link ProviderInterface} over the\n * daemon's `POST /api/chat`, assembling `generate` from the same NDJSON engine as `stream`.\n *\n * @remarks\n * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,\n * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling\n * parameters (`temperature`, `seed`, and `num_predict`). Each call takes an\n * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result.\n *\n * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):\n * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a\n * generated/obfuscated bearer token your server validates — so a browser runtime\n * reaches the LLM through your middleware without this library ever handling the real API\n * key. Both omitted ⇒ the global `fetch` and only a JSON content type.\n *\n * The optional `format` is the provider's context-framing default — the provider-default\n * level of `AgentContext`'s format cascade (beaten by a manager-options or per-item\n * override, beating the managers' built-in framing), declaring how this\n * provider's models prefer context sections framed (for example XML group wrappers vs. Markdown\n * headers). It is exposed on the provider for the Agent's `build()` and is not Ollama's\n * `/api/chat` `format` wire parameter (structured output) — the framing default and that\n * wire parameter are unrelated despite the shared word. Omitted ⇒ the provider is\n * framing-agnostic (core's built-in defaults).\n *\n * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /\n * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})\n * @returns A working {@link ProviderInterface} backed by Ollama\n *\n * @example createOllama + generate\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import type { TokenUsage } from '@orkestrel/budget'\n * import { createOllama } from '@orkestrel/ollama'\n *\n * declare function charge(usage: TokenUsage): void // your billing integration\n *\n * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', options: { temperature: 0 } })\n * const abort = createAbort()\n * const messages = [\n * \t{ id: '1', role: 'user', content: 'Summarize the release notes for version 2.0.' },\n * ] as const\n *\n * const result = await provider.generate(messages, abort.signal)\n * console.log(result.content)\n * if (result.usage) charge(result.usage) // fold into a token budget\n * ```\n *\n * @example\n * Route through your own server with an obfuscated token:\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * url: 'https://my-app.example.com/llm', // your server, not the daemon\n * fetch: myFetch, // optional custom transport\n * headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this\n * })\n * ```\n *\n * @example\n * Declare a context-framing default — wrap the instructions section in an XML group (the\n * provider-default level of `AgentContext`'s cascade; not the wire `format`):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * format: {\n * instructions: {\n * open: '<instructions>',\n * render: (i) => `<instruction>${i.content}</instruction>`,\n * close: '</instructions>',\n * },\n * },\n * })\n * ```\n */\nexport function createOllama(options: OllamaOptions): ProviderInterface {\n\treturn new OllamaProvider(options)\n}\n"],"mappings":"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"}
|