@warlock.js/ai-ollama 5.30.0 → 5.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/cjs/index.cjs +113 -16
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +3 -5
- package/esm/config.type.d.mts.map +1 -1
- package/esm/embedder.mjs.map +1 -1
- package/esm/index.d.mts +2 -1
- package/esm/model.mjs +43 -15
- package/esm/model.mjs.map +1 -1
- package/esm/provider-options.d.mts +8 -0
- package/esm/provider-options.d.mts.map +1 -0
- package/esm/provider-options.mjs +36 -0
- package/esm/provider-options.mjs.map +1 -0
- package/esm/sdk.d.mts +1 -3
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +6 -1
- package/esm/sdk.mjs.map +1 -1
- package/esm/utils/call-signal.mjs +40 -0
- package/esm/utils/call-signal.mjs.map +1 -0
- package/esm/utils/index.mjs +2 -1
- package/esm/utils/to-ollama-tools.mjs.map +1 -1
- package/llms-full.txt +69 -21
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/setup-ollama/SKILL.md +69 -21
package/esm/model.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { withCallSignal } from "./utils/call-signal.mjs";
|
|
1
2
|
import { mapDoneReason } from "./utils/map-done-reason.mjs";
|
|
2
3
|
import { toOllamaMessages } from "./utils/to-ollama-messages.mjs";
|
|
3
4
|
import { toOllamaTools } from "./utils/to-ollama-tools.mjs";
|
|
4
5
|
import { wrapOllamaError } from "./utils/wrap-ollama-error.mjs";
|
|
5
6
|
import "./utils/index.mjs";
|
|
7
|
+
import { mergeProviderBody } from "./provider-options.mjs";
|
|
6
8
|
import { inferReasoningCapability } from "./known-reasoning-models.mjs";
|
|
7
9
|
import { inferVisionCapability } from "./known-vision-models.mjs";
|
|
10
|
+
import { attachCost, resolveStreamChunks, selectProviderTools } from "@warlock.js/ai";
|
|
8
11
|
import { log } from "@warlock.js/logger";
|
|
9
12
|
|
|
10
13
|
//#region ../../packages/ai-ollama/src/model.ts
|
|
11
14
|
const LOG_MODULE = "ai.ollama";
|
|
15
|
+
/** `provider` value of `ai.providerTool()` entries this adapter sends verbatim. */
|
|
16
|
+
const OLLAMA_WIRE_FORMAT = "ollama";
|
|
12
17
|
/** Ollama supports only low, medium, and high named thinking levels. */
|
|
13
18
|
const EFFORT_TO_THINK_LEVEL = {
|
|
14
19
|
minimal: "low",
|
|
@@ -75,7 +80,8 @@ var OllamaModel = class {
|
|
|
75
80
|
* Single-shot completion. Sends the full message list to
|
|
76
81
|
* `client.chat`, waits for the terminal response, and reshapes it
|
|
77
82
|
* into a vendor-neutral `ModelResponse`. Per-call `options` override
|
|
78
|
-
* the instance defaults for this call only.
|
|
83
|
+
* the instance defaults for this call only. Aborting `options.signal`
|
|
84
|
+
* cancels the HTTP request through the SDK client's signal-aware `fetch`.
|
|
79
85
|
*/
|
|
80
86
|
async complete(messages, options) {
|
|
81
87
|
this.logger.debug(LOG_MODULE, "request", "Starting chat call", {
|
|
@@ -86,16 +92,17 @@ var OllamaModel = class {
|
|
|
86
92
|
});
|
|
87
93
|
let response;
|
|
88
94
|
try {
|
|
89
|
-
|
|
95
|
+
options?.signal?.throwIfAborted();
|
|
96
|
+
response = await withCallSignal(options?.signal, () => this.client.chat({
|
|
90
97
|
...this.buildRequest(messages, options),
|
|
91
98
|
stream: false
|
|
92
|
-
});
|
|
99
|
+
}));
|
|
93
100
|
} catch (thrown) {
|
|
94
101
|
throw this.logAndWrap(thrown);
|
|
95
102
|
}
|
|
96
103
|
const toolCalls = this.extractToolCalls(response.message);
|
|
97
104
|
const finishReason = toolCalls ? "tool_calls" : mapDoneReason(response.done_reason);
|
|
98
|
-
const usage = this.extractUsage(response);
|
|
105
|
+
const usage = attachCost(this.extractUsage(response), this.pricing);
|
|
99
106
|
this.logger.debug(LOG_MODULE, "response", "chat call succeeded", {
|
|
100
107
|
finishReason,
|
|
101
108
|
usage
|
|
@@ -112,7 +119,12 @@ var OllamaModel = class {
|
|
|
112
119
|
* `ModelStreamChunk`s — `delta` for content, `tool-call` per
|
|
113
120
|
* function call (Ollama streams a fully-formed call, not partial
|
|
114
121
|
* JSON), and a terminal `done` with the final finish reason + usage.
|
|
115
|
-
* Honors `options.signal
|
|
122
|
+
* Honors `options.signal`: aborting cancels the HTTP request (through
|
|
123
|
+
* the SDK client's signal-aware `fetch`) and the stream iterator.
|
|
124
|
+
*
|
|
125
|
+
* With `options.streamChunks` it also yields `reasoning` for each
|
|
126
|
+
* `message.thinking` piece. Usage arrives only on the final chunk, so no
|
|
127
|
+
* `usage` chunk; calls arrive whole, so no `tool-args`.
|
|
116
128
|
*/
|
|
117
129
|
async *stream(messages, options) {
|
|
118
130
|
this.logger.debug(LOG_MODULE, "request", "Starting streaming chat call", {
|
|
@@ -123,15 +135,18 @@ var OllamaModel = class {
|
|
|
123
135
|
});
|
|
124
136
|
let stream;
|
|
125
137
|
try {
|
|
126
|
-
|
|
138
|
+
options?.signal?.throwIfAborted();
|
|
139
|
+
stream = await withCallSignal(options?.signal, () => this.client.chat({
|
|
127
140
|
...this.buildRequest(messages, options),
|
|
128
141
|
stream: true
|
|
129
|
-
});
|
|
142
|
+
}));
|
|
130
143
|
} catch (thrown) {
|
|
131
144
|
throw this.logAndWrap(thrown);
|
|
132
145
|
}
|
|
133
|
-
if (options?.signal)
|
|
134
|
-
|
|
146
|
+
if (options?.signal) {
|
|
147
|
+
if (options.signal.aborted) stream.abort();
|
|
148
|
+
else options.signal.addEventListener("abort", () => stream.abort(), { once: true });
|
|
149
|
+
}
|
|
135
150
|
let rawDoneReason;
|
|
136
151
|
let sawToolCall = false;
|
|
137
152
|
const usage = {
|
|
@@ -139,8 +154,14 @@ var OllamaModel = class {
|
|
|
139
154
|
output: 0,
|
|
140
155
|
total: 0
|
|
141
156
|
};
|
|
157
|
+
const want = resolveStreamChunks(options);
|
|
142
158
|
try {
|
|
143
159
|
for await (const chunk of stream) {
|
|
160
|
+
const thinking = chunk.message?.thinking;
|
|
161
|
+
if (want.reasoning && thinking) yield {
|
|
162
|
+
type: "reasoning",
|
|
163
|
+
delta: thinking
|
|
164
|
+
};
|
|
144
165
|
const content = chunk.message?.content;
|
|
145
166
|
if (content) yield {
|
|
146
167
|
type: "delta",
|
|
@@ -166,6 +187,7 @@ var OllamaModel = class {
|
|
|
166
187
|
throw this.logAndWrap(thrown);
|
|
167
188
|
}
|
|
168
189
|
const finishReason = sawToolCall ? "tool_calls" : mapDoneReason(rawDoneReason);
|
|
190
|
+
attachCost(usage, this.pricing);
|
|
169
191
|
this.logger.debug(LOG_MODULE, "response", "streaming chat call succeeded", {
|
|
170
192
|
finishReason,
|
|
171
193
|
usage
|
|
@@ -189,14 +211,14 @@ var OllamaModel = class {
|
|
|
189
211
|
...temperature !== void 0 ? { temperature } : {},
|
|
190
212
|
...maxTokens !== void 0 ? { num_predict: maxTokens } : {}
|
|
191
213
|
};
|
|
192
|
-
return {
|
|
214
|
+
return mergeProviderBody({
|
|
193
215
|
model: this.name,
|
|
194
216
|
messages: toOllamaMessages(messages),
|
|
195
217
|
...Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {},
|
|
196
|
-
...this.buildTools(options
|
|
218
|
+
...this.buildTools(options),
|
|
197
219
|
...this.buildFormat(options?.responseSchema),
|
|
198
220
|
...this.buildThink(options?.reasoning)
|
|
199
|
-
};
|
|
221
|
+
}, options);
|
|
200
222
|
}
|
|
201
223
|
/**
|
|
202
224
|
* Translate the neutral `reasoning` hint into Ollama's `think`
|
|
@@ -226,9 +248,15 @@ var OllamaModel = class {
|
|
|
226
248
|
* Spread-friendly tools fragment. Empty object when no tools were
|
|
227
249
|
* supplied so the caller can unconditionally spread it.
|
|
228
250
|
*/
|
|
229
|
-
buildTools(
|
|
230
|
-
const
|
|
231
|
-
|
|
251
|
+
buildTools(options) {
|
|
252
|
+
const { tools, definitions } = selectProviderTools(options, {
|
|
253
|
+
accepts: [OLLAMA_WIRE_FORMAT, this.provider],
|
|
254
|
+
module: LOG_MODULE,
|
|
255
|
+
model: this.name,
|
|
256
|
+
logger: this.logger
|
|
257
|
+
});
|
|
258
|
+
const mapped = [...toOllamaTools(tools) ?? [], ...definitions];
|
|
259
|
+
return mapped.length > 0 ? { tools: mapped } : {};
|
|
232
260
|
}
|
|
233
261
|
/**
|
|
234
262
|
* Translate the neutral `responseSchema` into Ollama's native
|
package/esm/model.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/model.ts"],"sourcesContent":["import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n AbortableAsyncIterator,\n ChatRequest,\n ChatResponse,\n Ollama,\n Options,\n} from \"ollama\";\nimport type { OllamaModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapDoneReason, toOllamaMessages, toOllamaTools, wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/** Ollama supports only low, medium, and high named thinking levels. */\nconst EFFORT_TO_THINK_LEVEL: Record<\n Exclude<ReasoningEffort, \"none\">,\n \"low\" | \"medium\" | \"high\"\n> = {\n minimal: \"low\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"high\",\n max: \"high\",\n};\n\n/**\n * Ollama-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and a local (or self-hosted) Ollama\n * server via the official `ollama` client.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client + frozen `ModelConfig` (model\n * tag, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Ollama's chat shapes (system stays a real role, `tool_calls` /\n * `tool_name`, base64 `images`) and Ollama's response (content, tool\n * calls, done reason, eval-count usage) back into neutral shapes.\n * - Does NOT own: tool dispatch, looping, history, retries — agent\n * concerns. The model is a per-call protocol adapter.\n *\n * **Tool-call ids.** Ollama has no tool-call id concept — a `tool_call`\n * is `{ function: { name, arguments } }`. The adapter synthesizes the\n * neutral `id` from the tool name so the agent's tool-result round-trip\n * (which keys on `toolCallId`) maps back to Ollama's name-based\n * matching. Parallel calls to the *same* tool in one turn therefore\n * share an id — a documented v1 limitation inherent to Ollama's wire\n * format, not this adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\").\n *\n * @example\n * import { Ollama } from \"ollama\";\n * const client = new Ollama({ host: \"http://127.0.0.1:11434\" });\n * const model = new OllamaModel(client, { name: \"llama3.1\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OllamaModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: Ollama;\n private readonly config: OllamaModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: Ollama, config: OllamaModelConfig, provider: string = \"ollama\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n // Thinking-capable families (deepseek-r1, qwq, qwen3, …) honor the\n // `think` request flag; plain instruct models do not. Explicit\n // config wins over the family-substring inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Ollama has no provider-side prompt cache and the chat API takes\n // no audio / PDF content parts — report these truthfully as false\n // so the agent rejects unsupported attachments / cache hints\n // upfront instead of silently dropping them at the wire.\n promptCaching: false,\n audio: false,\n pdf: false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `client.chat`, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override\n * the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: ChatResponse;\n\n try {\n response = await this.client.chat({ ...this.buildRequest(messages, options), stream: false });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response.message);\n const finishReason = toolCalls ? \"tool_calls\" : mapDoneReason(response.done_reason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"chat call succeeded\", { finishReason, usage });\n\n return {\n content: response.message?.content ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral\n * `ModelStreamChunk`s — `delta` for content, `tool-call` per\n * function call (Ollama streams a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n * Honors `options.signal` by aborting the underlying stream.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: AbortableAsyncIterator<ChatResponse>;\n\n try {\n stream = await this.client.chat({ ...this.buildRequest(messages, options), stream: true });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n stream.abort();\n } else {\n options.signal.addEventListener(\"abort\", () => stream.abort(), { once: true });\n }\n }\n\n let rawDoneReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of stream) {\n const content = chunk.message?.content;\n\n if (content) {\n yield { type: \"delta\", content };\n }\n\n for (const call of chunk.message?.tool_calls ?? []) {\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n };\n }\n\n if (chunk.done_reason) {\n rawDoneReason = chunk.done_reason;\n }\n\n if (chunk.done) {\n usage.input = chunk.prompt_eval_count ?? usage.input;\n usage.output = chunk.eval_count ?? usage.output;\n usage.total = usage.input + usage.output;\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapDoneReason(rawDoneReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"streaming chat call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the Ollama chat request shared by `complete()` and\n * `stream()` (each adds its own `stream` literal so the client's\n * overload resolves). Maps inference params into Ollama `options`\n * and conditionally attaches tools + native structured output.\n */\n private buildRequest(\n messages: Message[],\n options: ModelCallOptions | undefined,\n ): Omit<ChatRequest, \"stream\"> {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n\n const ollamaOptions: Partial<Options> = {\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxTokens !== undefined ? { num_predict: maxTokens } : {}),\n };\n\n return {\n model: this.name,\n messages: toOllamaMessages(messages),\n ...(Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildFormat(options?.responseSchema),\n ...this.buildThink(options?.reasoning),\n };\n }\n\n /**\n * Translate the neutral `reasoning` hint into Ollama's `think`\n * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'\n * | 'high'`, so neutral effort levels are clamped to the nearest named\n * level; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\n * The neutral `effort: \"none\"` (\"run without reasoning\") maps to\n * `think: false` — Ollama has no `\"none\"` level, and this is its\n * native reasoning-off switch.\n *\n * No-ops unless the model is reasoning-capable, so the `think` flag is\n * never sent to a plain instruct model that cannot honor it.\n *\n * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama\n * equivalent — the daemon does not accept a thinking-token cap — so it\n * is honored only as the on/off signal above and otherwise ignored.\n * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no\n * provider prompt cache, so there is no cache breakpoint to place.\n */\n private buildThink(\n reasoning: ModelCallOptions[\"reasoning\"],\n ): Pick<ChatRequest, \"think\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit reasoning-off → switch the channel off.\n // Ollama's `think` has no `\"none\"` level, so map it to `false`.\n if (reasoning.effort === \"none\") {\n return { think: false };\n }\n\n return { think: reasoning.effort ? EFFORT_TO_THINK_LEVEL[reasoning.effort] : true };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<ChatRequest, \"tools\"> {\n const mapped = toOllamaTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Ollama's native\n * structured output (`format` accepts a JSON Schema object).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildFormat(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<ChatRequest, \"format\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return { format: responseSchema };\n }\n\n /**\n * Reshape Ollama's `message.tool_calls` into the neutral\n * `ModelToolCallRequest[]`. Ollama has no tool-call id, so the\n * neutral `id` is synthesized from the tool name (see the class\n * doc). Returns `undefined` when no tools were requested.\n */\n private extractToolCalls(\n message: ChatResponse[\"message\"] | undefined,\n ): ModelToolCallRequest[] | undefined {\n const calls = message?.tool_calls;\n\n if (!calls || calls.length === 0) {\n return undefined;\n }\n\n return calls.map((call) => ({\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n }));\n }\n\n /**\n * Normalize Ollama's eval counts into the neutral `Usage` shape.\n *\n * Cost-truth: Ollama reports only `prompt_eval_count` (input) and\n * `eval_count` (output). It has **no** provider prompt cache, so\n * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined\n * (honest absence, not a false zero). Reasoning models emit their\n * thinking as the `message.thinking` *string* but the wire format\n * carries **no separate reasoning-token count** — the thinking tokens\n * are already folded into `eval_count`. We therefore do not fabricate\n * a `Usage.reasoningTokens` from the text length; it is left undefined\n * unless/until the daemon exposes a real count. `total` is input +\n * output.\n */\n private extractUsage(response: ChatResponse): Usage {\n const input = response.prompt_eval_count ?? 0;\n const output = response.eval_count ?? 0;\n\n return { input, output, total: input + output };\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/model.ts"],"sourcesContent":["import {\n attachCost,\n resolveStreamChunks,\n selectProviderTools,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n AbortableAsyncIterator,\n ChatRequest,\n ChatResponse,\n Ollama,\n Options,\n Tool,\n} from \"ollama\";\nimport { mergeProviderBody } from \"./provider-options\";\nimport type { OllamaModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport {\n mapDoneReason,\n toOllamaMessages,\n toOllamaTools,\n withCallSignal,\n wrapOllamaError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/** `provider` value of `ai.providerTool()` entries this adapter sends verbatim. */\nconst OLLAMA_WIRE_FORMAT = \"ollama\";\n\n/** Ollama supports only low, medium, and high named thinking levels. */\nconst EFFORT_TO_THINK_LEVEL: Record<Exclude<ReasoningEffort, \"none\">, \"low\" | \"medium\" | \"high\"> = {\n minimal: \"low\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"high\",\n max: \"high\",\n};\n\n/**\n * Ollama-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and a local (or self-hosted) Ollama\n * server via the official `ollama` client.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client + frozen `ModelConfig` (model\n * tag, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Ollama's chat shapes (system stays a real role, `tool_calls` /\n * `tool_name`, base64 `images`) and Ollama's response (content, tool\n * calls, done reason, eval-count usage) back into neutral shapes.\n * - Does NOT own: tool dispatch, looping, history, retries — agent\n * concerns. The model is a per-call protocol adapter.\n *\n * **Tool-call ids.** Ollama has no tool-call id concept — a `tool_call`\n * is `{ function: { name, arguments } }`. The adapter synthesizes the\n * neutral `id` from the tool name so the agent's tool-result round-trip\n * (which keys on `toolCallId`) maps back to Ollama's name-based\n * matching. Parallel calls to the *same* tool in one turn therefore\n * share an id — a documented v1 limitation inherent to Ollama's wire\n * format, not this adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\").\n *\n * @example\n * import { Ollama } from \"ollama\";\n * const client = new Ollama({ host: \"http://127.0.0.1:11434\" });\n * const model = new OllamaModel(client, { name: \"llama3.1\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OllamaModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: Ollama;\n private readonly config: OllamaModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: Ollama, config: OllamaModelConfig, provider: string = \"ollama\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n // Thinking-capable families (deepseek-r1, qwq, qwen3, …) honor the\n // `think` request flag; plain instruct models do not. Explicit\n // config wins over the family-substring inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Ollama has no provider-side prompt cache and the chat API takes\n // no audio / PDF content parts — report these truthfully as false\n // so the agent rejects unsupported attachments / cache hints\n // upfront instead of silently dropping them at the wire.\n promptCaching: false,\n audio: false,\n pdf: false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `client.chat`, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override\n * the instance defaults for this call only. Aborting `options.signal`\n * cancels the HTTP request through the SDK client's signal-aware `fetch`.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: ChatResponse;\n\n try {\n options?.signal?.throwIfAborted();\n response = await withCallSignal(options?.signal, () =>\n this.client.chat({ ...this.buildRequest(messages, options), stream: false }),\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response.message);\n const finishReason = toolCalls ? \"tool_calls\" : mapDoneReason(response.done_reason);\n const usage = attachCost(this.extractUsage(response), this.pricing);\n\n this.logger.debug(LOG_MODULE, \"response\", \"chat call succeeded\", { finishReason, usage });\n\n return {\n content: response.message?.content ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral\n * `ModelStreamChunk`s — `delta` for content, `tool-call` per\n * function call (Ollama streams a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n * Honors `options.signal`: aborting cancels the HTTP request (through\n * the SDK client's signal-aware `fetch`) and the stream iterator.\n *\n * With `options.streamChunks` it also yields `reasoning` for each\n * `message.thinking` piece. Usage arrives only on the final chunk, so no\n * `usage` chunk; calls arrive whole, so no `tool-args`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: AbortableAsyncIterator<ChatResponse>;\n\n try {\n options?.signal?.throwIfAborted();\n stream = await withCallSignal(options?.signal, () =>\n this.client.chat({ ...this.buildRequest(messages, options), stream: true }),\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n stream.abort();\n } else {\n options.signal.addEventListener(\"abort\", () => stream.abort(), { once: true });\n }\n }\n\n let rawDoneReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const want = resolveStreamChunks(options);\n\n try {\n for await (const chunk of stream) {\n // `message.thinking` streams when the request sets `think`.\n const thinking = chunk.message?.thinking;\n\n if (want.reasoning && thinking) {\n yield { type: \"reasoning\", delta: thinking };\n }\n\n const content = chunk.message?.content;\n\n if (content) {\n yield { type: \"delta\", content };\n }\n\n for (const call of chunk.message?.tool_calls ?? []) {\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n };\n }\n\n if (chunk.done_reason) {\n rawDoneReason = chunk.done_reason;\n }\n\n if (chunk.done) {\n usage.input = chunk.prompt_eval_count ?? usage.input;\n usage.output = chunk.eval_count ?? usage.output;\n usage.total = usage.input + usage.output;\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapDoneReason(rawDoneReason);\n\n attachCost(usage, this.pricing);\n\n this.logger.debug(LOG_MODULE, \"response\", \"streaming chat call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the Ollama chat request shared by `complete()` and\n * `stream()` (each adds its own `stream` literal so the client's\n * overload resolves). Maps inference params into Ollama `options`\n * and conditionally attaches tools + native structured output.\n */\n private buildRequest(\n messages: Message[],\n options: ModelCallOptions | undefined,\n ): Omit<ChatRequest, \"stream\"> {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n\n const ollamaOptions: Partial<Options> = {\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxTokens !== undefined ? { num_predict: maxTokens } : {}),\n };\n\n return mergeProviderBody<Omit<ChatRequest, \"stream\">>(\n {\n model: this.name,\n messages: toOllamaMessages(messages),\n ...(Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {}),\n ...this.buildTools(options),\n ...this.buildFormat(options?.responseSchema),\n ...this.buildThink(options?.reasoning),\n },\n options,\n );\n }\n\n /**\n * Translate the neutral `reasoning` hint into Ollama's `think`\n * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'\n * | 'high'`, so neutral effort levels are clamped to the nearest named\n * level; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\n * The neutral `effort: \"none\"` (\"run without reasoning\") maps to\n * `think: false` — Ollama has no `\"none\"` level, and this is its\n * native reasoning-off switch.\n *\n * No-ops unless the model is reasoning-capable, so the `think` flag is\n * never sent to a plain instruct model that cannot honor it.\n *\n * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama\n * equivalent — the daemon does not accept a thinking-token cap — so it\n * is honored only as the on/off signal above and otherwise ignored.\n * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no\n * provider prompt cache, so there is no cache breakpoint to place.\n */\n private buildThink(reasoning: ModelCallOptions[\"reasoning\"]): Pick<ChatRequest, \"think\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit reasoning-off → switch the channel off.\n // Ollama's `think` has no `\"none\"` level, so map it to `false`.\n if (reasoning.effort === \"none\") {\n return { think: false };\n }\n\n return { think: reasoning.effort ? EFFORT_TO_THINK_LEVEL[reasoning.effort] : true };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(options: ModelCallOptions | undefined): Pick<ChatRequest, \"tools\"> {\n // Ollama's chat API has no built-in tools; provider-defined entries\n // reach it only when they name this adapter, and others are skipped\n // with a warning.\n const { tools, definitions } = selectProviderTools(options, {\n accepts: [OLLAMA_WIRE_FORMAT, this.provider],\n module: LOG_MODULE,\n model: this.name,\n logger: this.logger,\n });\n const mapped = [...(toOllamaTools(tools) ?? []), ...(definitions as unknown as Tool[])];\n\n return mapped.length > 0 ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Ollama's native\n * structured output (`format` accepts a JSON Schema object).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildFormat(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<ChatRequest, \"format\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return { format: responseSchema };\n }\n\n /**\n * Reshape Ollama's `message.tool_calls` into the neutral\n * `ModelToolCallRequest[]`. Ollama has no tool-call id, so the\n * neutral `id` is synthesized from the tool name (see the class\n * doc). Returns `undefined` when no tools were requested.\n */\n private extractToolCalls(\n message: ChatResponse[\"message\"] | undefined,\n ): ModelToolCallRequest[] | undefined {\n const calls = message?.tool_calls;\n\n if (!calls || calls.length === 0) {\n return undefined;\n }\n\n return calls.map((call) => ({\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n }));\n }\n\n /**\n * Normalize Ollama's eval counts into the neutral `Usage` shape.\n *\n * Cost-truth: Ollama reports only `prompt_eval_count` (input) and\n * `eval_count` (output). It has **no** provider prompt cache, so\n * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined\n * (honest absence, not a false zero). Reasoning models emit their\n * thinking as the `message.thinking` *string* but the wire format\n * carries **no separate reasoning-token count** — the thinking tokens\n * are already folded into `eval_count`. We therefore do not fabricate\n * a `Usage.reasoningTokens` from the text length; it is left undefined\n * unless/until the daemon exposes a real count. `total` is input +\n * output.\n */\n private extractUsage(response: ChatResponse): Usage {\n const input = response.prompt_eval_count ?? 0;\n const output = response.eval_count ?? 0;\n\n return { input, output, total: input + output };\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "@warlock.js/ai";
|
|
2
|
+
//#region ../../packages/ai-ollama/src/provider-options.d.ts
|
|
3
|
+
/** Adapter-only fields merged after the neutral request parameters. */
|
|
4
|
+
export type OllamaCallOptions = {
|
|
5
|
+
body?: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
//# sourceMappingURL=provider-options.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-options.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/provider-options.ts"],"mappings":";;;YAGY;EAAsB,OAAO"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { InvalidRequestError } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/ai-ollama/src/provider-options.ts
|
|
4
|
+
const RESERVED_FIELDS = /* @__PURE__ */ new Set([
|
|
5
|
+
"model",
|
|
6
|
+
"messages",
|
|
7
|
+
"tools",
|
|
8
|
+
"stream"
|
|
9
|
+
]);
|
|
10
|
+
function mergeProviderBody(built, options) {
|
|
11
|
+
const body = (options?.providerOptions?.ollama)?.body ?? {};
|
|
12
|
+
const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));
|
|
13
|
+
if (reserved.length > 0) throw new InvalidRequestError(`providerOptions.ollama.body cannot set ${reserved.join(", ")}: use the call's messages, tools and model options.`, { context: { fields: reserved } });
|
|
14
|
+
const defaults = built;
|
|
15
|
+
const overrides = Object.fromEntries(Object.entries(body).map(([key, value]) => {
|
|
16
|
+
const previous = defaults[key];
|
|
17
|
+
return [key, isPlainObject(previous) && isPlainObject(value) ? {
|
|
18
|
+
...previous,
|
|
19
|
+
...value
|
|
20
|
+
} : value];
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
...built,
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Merge only plain objects; arrays, null and provider scalar values replace the field. */
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
if (value === null || typeof value !== "object") return false;
|
|
30
|
+
const prototype = Object.getPrototypeOf(value);
|
|
31
|
+
return prototype === Object.prototype || prototype === null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { mergeProviderBody };
|
|
36
|
+
//# sourceMappingURL=provider-options.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-options.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/provider-options.ts"],"sourcesContent":["import { InvalidRequestError, type ModelCallOptions } from \"@warlock.js/ai\";\n\n/** Adapter-only fields merged after the neutral request parameters. */\nexport type OllamaCallOptions = { body?: Record<string, unknown> };\n\nconst RESERVED_FIELDS = new Set([\"model\", \"messages\", \"tools\", \"stream\"]);\n\nexport function mergeProviderBody<T extends object>(\n built: T,\n options: ModelCallOptions | undefined,\n): T {\n const provider = options?.providerOptions?.ollama as OllamaCallOptions | undefined;\n const body = provider?.body ?? {};\n const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));\n if (reserved.length > 0) {\n throw new InvalidRequestError(\n `providerOptions.ollama.body cannot set ${reserved.join(\", \")}: use the call's messages, tools and model options.`,\n { context: { fields: reserved } },\n );\n }\n const defaults = built as Record<string, unknown>;\n const overrides = Object.fromEntries(\n Object.entries(body).map(([key, value]) => {\n const previous = defaults[key];\n return [\n key,\n isPlainObject(previous) && isPlainObject(value) ? { ...previous, ...value } : value,\n ];\n }),\n );\n return { ...built, ...overrides };\n}\n\n/** Merge only plain objects; arrays, null and provider scalar values replace the field. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n"],"mappings":";;;AAKA,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAS;CAAY;CAAS;AAAQ,CAAC;AAExE,SAAgB,kBACd,OACA,SACG;CAEH,MAAM,QADW,SAAS,iBAAiB,OACtB,EAAE,QAAQ,CAAC;CAChC,MAAM,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,QAAQ,gBAAgB,IAAI,GAAG,CAAC;CAC3E,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,oBACR,0CAA0C,SAAS,KAAK,IAAI,EAAE,sDAC9D,EAAE,SAAS,EAAE,QAAQ,SAAS,EAAE,CAClC;CAEF,MAAM,WAAW;CACjB,MAAM,YAAY,OAAO,YACvB,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW;EACzC,MAAM,WAAW,SAAS;EAC1B,OAAO,CACL,KACA,cAAc,QAAQ,KAAK,cAAc,KAAK,IAAI;GAAE,GAAG;GAAU,GAAG;EAAM,IAAI,KAChF;CACF,CAAC,CACH;CACA,OAAO;EAAE,GAAG;EAAO,GAAG;CAAU;AAClC;;AAGA,SAAS,cAAc,OAAkD;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD"}
|
package/esm/sdk.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { OllamaEmbedderConfig, OllamaModelConfig, OllamaSDKConfig } from "./config.type.mjs";
|
|
2
2
|
import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
|
|
3
|
-
|
|
4
3
|
//#region ../../packages/ai-ollama/src/sdk.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Ollama-backed implementation of `SDKAdapterContract`.
|
|
@@ -25,7 +24,7 @@ import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js
|
|
|
25
24
|
* const model = ollama.model({ name: "llama3.1", temperature: 0.7 });
|
|
26
25
|
* const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
27
26
|
*/
|
|
28
|
-
declare class OllamaSDK implements SDKAdapterContract {
|
|
27
|
+
export declare class OllamaSDK implements SDKAdapterContract {
|
|
29
28
|
private readonly client;
|
|
30
29
|
private readonly provider;
|
|
31
30
|
private readonly pricing?;
|
|
@@ -58,5 +57,4 @@ declare class OllamaSDK implements SDKAdapterContract {
|
|
|
58
57
|
embedder(config: OllamaEmbedderConfig): EmbedderContract;
|
|
59
58
|
}
|
|
60
59
|
//#endregion
|
|
61
|
-
export { OllamaSDK };
|
|
62
60
|
//# sourceMappingURL=sdk.d.mts.map
|
package/esm/sdk.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;qBAoCa,qBAAqB;mBACf;mBACA;mBACA;EAEjB,YAAmB,SAAQ;;;;;;;;;;EAuBpB,MAAM,QAAQ,oBAAoB;;;;;;;;EAe5B,MAAM,cAAc,kBAAkB;;;;;;;;EAW5C,SAAS,QAAQ,uBAAuB"}
|
package/esm/sdk.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { signalAwareFetch } from "./utils/call-signal.mjs";
|
|
2
|
+
import "./utils/index.mjs";
|
|
1
3
|
import { OllamaEmbedder } from "./embedder.mjs";
|
|
2
4
|
import { OllamaModel } from "./model.mjs";
|
|
3
5
|
import { Ollama } from "ollama";
|
|
@@ -30,7 +32,10 @@ import { approximateTokenCount } from "@warlock.js/ai";
|
|
|
30
32
|
var OllamaSDK = class {
|
|
31
33
|
constructor(config = {}) {
|
|
32
34
|
const { provider, pricing, ...clientConfig } = config;
|
|
33
|
-
this.client = new Ollama(
|
|
35
|
+
this.client = new Ollama({
|
|
36
|
+
...clientConfig,
|
|
37
|
+
fetch: signalAwareFetch(clientConfig.fetch ?? globalThis.fetch)
|
|
38
|
+
});
|
|
34
39
|
this.provider = provider ?? "ollama";
|
|
35
40
|
this.pricing = pricing;
|
|
36
41
|
}
|
package/esm/sdk.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"sourcesContent":["import { Ollama } from \"ollama\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {
|
|
1
|
+
{"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"sourcesContent":["import { Ollama } from \"ollama\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OllamaEmbedderConfig, OllamaModelConfig, OllamaSDKConfig } from \"./config.type\";\nimport { OllamaEmbedder } from \"./embedder\";\nimport { OllamaModel } from \"./model\";\nimport { signalAwareFetch } from \"./utils\";\n\n/**\n * Ollama-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for local / self-hosted models\n * served by an Ollama daemon via the official `ollama` client. One\n * `OllamaSDK` holds one live `Ollama` client, shared by every\n * `ModelContract` / `EmbedderContract` it produces.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client (host, headers) and its\n * lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances\n * sharing that client.\n * - Does NOT own: anything per-call — those live in `OllamaModel` /\n * `OllamaEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const ollama = new OllamaSDK({}); // local default host\n * const model = ollama.model({ name: \"llama3.1\", temperature: 0.7 });\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n */\nexport class OllamaSDK implements SDKAdapterContract {\n private readonly client: Ollama;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OllamaSDKConfig = {}) {\n const { provider, pricing, ...clientConfig } = config;\n\n // Route every request through a fetch that honours the current call's\n // `ModelCallOptions.signal` — the `ollama` client takes no per-request\n // signal of its own (see `utils/call-signal.ts`).\n this.client = new Ollama({\n ...clientConfig,\n fetch: signalAwareFetch(clientConfig.fetch ?? globalThis.fetch),\n });\n this.provider = provider ?? \"ollama\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OllamaModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `Ollama` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (local Ollama is free, so usually undefined).\n */\n public model(config: OllamaModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OllamaModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OllamaModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — good enough for\n * budgeting / context guards, not billing (and Ollama is free\n * anyway). The optional model id is reserved for future per-model\n * tokenizer dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OllamaEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OllamaEmbedderConfig): EmbedderContract {\n return new OllamaEmbedder(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,SAA0B,CAAC,GAAG;EAC/C,MAAM,EAAE,UAAU,SAAS,GAAG,iBAAiB;EAK/C,KAAK,SAAS,IAAI,OAAO;GACvB,GAAG;GACH,OAAO,iBAAiB,aAAa,SAAS,WAAW,KAAK;EAChE,CAAC;EACD,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CAC9D;AACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/ai-ollama/src/utils/call-signal.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-call cancellation for the `ollama` client.
|
|
6
|
+
*
|
|
7
|
+
* The `ollama` client (0.6) takes no per-request `AbortSignal`: a
|
|
8
|
+
* non-streaming `chat()` cannot be cancelled at all, and a streamed one only
|
|
9
|
+
* through the iterator it returns once the response headers have arrived.
|
|
10
|
+
* To honour `ModelCallOptions.signal` the SDK builds its client with
|
|
11
|
+
* {@link signalAwareFetch}, and the model runs each request inside
|
|
12
|
+
* {@link withCallSignal}; the wrapped `fetch` reads the call's signal from
|
|
13
|
+
* async context and hands it to the real `fetch`, so aborting cancels the
|
|
14
|
+
* HTTP request at any point — before the headers, or mid-body.
|
|
15
|
+
*/
|
|
16
|
+
const callSignal = new AsyncLocalStorage();
|
|
17
|
+
/** Run `operation` with `signal` as the cancellation signal for any `fetch` it makes through {@link signalAwareFetch}. */
|
|
18
|
+
function withCallSignal(signal, operation) {
|
|
19
|
+
return callSignal.run(signal, operation);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a `fetch` so every request also listens to the signal of the call it
|
|
23
|
+
* belongs to (see {@link withCallSignal}), combined with any signal the
|
|
24
|
+
* client set itself.
|
|
25
|
+
*/
|
|
26
|
+
function signalAwareFetch(baseFetch) {
|
|
27
|
+
return (input, init) => {
|
|
28
|
+
const signal = callSignal.getStore();
|
|
29
|
+
if (!signal) return baseFetch(input, init);
|
|
30
|
+
const own = init?.signal ?? void 0;
|
|
31
|
+
return baseFetch(input, {
|
|
32
|
+
...init,
|
|
33
|
+
signal: own ? AbortSignal.any([own, signal]) : signal
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { signalAwareFetch, withCallSignal };
|
|
40
|
+
//# sourceMappingURL=call-signal.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-signal.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/call-signal.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Per-call cancellation for the `ollama` client.\n *\n * The `ollama` client (0.6) takes no per-request `AbortSignal`: a\n * non-streaming `chat()` cannot be cancelled at all, and a streamed one only\n * through the iterator it returns once the response headers have arrived.\n * To honour `ModelCallOptions.signal` the SDK builds its client with\n * {@link signalAwareFetch}, and the model runs each request inside\n * {@link withCallSignal}; the wrapped `fetch` reads the call's signal from\n * async context and hands it to the real `fetch`, so aborting cancels the\n * HTTP request at any point — before the headers, or mid-body.\n */\nconst callSignal = new AsyncLocalStorage<AbortSignal | undefined>();\n\n/** Run `operation` with `signal` as the cancellation signal for any `fetch` it makes through {@link signalAwareFetch}. */\nexport function withCallSignal<T>(signal: AbortSignal | undefined, operation: () => T): T {\n return callSignal.run(signal, operation);\n}\n\n/**\n * Wrap a `fetch` so every request also listens to the signal of the call it\n * belongs to (see {@link withCallSignal}), combined with any signal the\n * client set itself.\n */\nexport function signalAwareFetch(baseFetch: typeof fetch): typeof fetch {\n return (input, init) => {\n const signal = callSignal.getStore();\n\n if (!signal) {\n return baseFetch(input, init);\n }\n\n const own = init?.signal ?? undefined;\n\n return baseFetch(input, {\n ...init,\n signal: own ? AbortSignal.any([own, signal]) : signal,\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,aAAa,IAAI,kBAA2C;;AAGlE,SAAgB,eAAkB,QAAiC,WAAuB;CACxF,OAAO,WAAW,IAAI,QAAQ,SAAS;AACzC;;;;;;AAOA,SAAgB,iBAAiB,WAAuC;CACtE,QAAQ,OAAO,SAAS;EACtB,MAAM,SAAS,WAAW,SAAS;EAEnC,IAAI,CAAC,QACH,OAAO,UAAU,OAAO,IAAI;EAG9B,MAAM,MAAM,MAAM,UAAU;EAE5B,OAAO,UAAU,OAAO;GACtB,GAAG;GACH,QAAQ,MAAM,YAAY,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI;EACjD,CAAC;CACH;AACF"}
|
package/esm/utils/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { signalAwareFetch, withCallSignal } from "./call-signal.mjs";
|
|
1
2
|
import { mapDoneReason } from "./map-done-reason.mjs";
|
|
2
3
|
import { toOllamaMessages } from "./to-ollama-messages.mjs";
|
|
3
4
|
import { toOllamaTools } from "./to-ollama-tools.mjs";
|
|
4
5
|
import { wrapOllamaError } from "./wrap-ollama-error.mjs";
|
|
5
6
|
|
|
6
|
-
export {
|
|
7
|
+
export { mapDoneReason, signalAwareFetch, toOllamaMessages, toOllamaTools, withCallSignal, wrapOllamaError };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-ollama-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/to-ollama-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"ollama\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.\n * Each tool becomes a `{ type: \"function\", function: { name,\n * description, parameters } }` entry. Non-object extractions degrade\n * to a parameterless object so registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `tools` from the request.\n *\n * @example\n * const tools = toOllamaTools([weatherTool]);\n * await ollama.chat({ model, messages, tools });\n */\nexport function toOllamaTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Ollama wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Tool[\"function\"][\"parameters\"] {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema as Tool[\"function\"][\"parameters\"];\n }\n\n return { type: \"object\" } as Tool[\"function\"][\"parameters\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;AAOA,SAAS,
|
|
1
|
+
{"version":3,"file":"to-ollama-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/to-ollama-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"ollama\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.\n * Each tool becomes a `{ type: \"function\", function: { name,\n * description, parameters } }` entry. Non-object extractions degrade\n * to a parameterless object so registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `tools` from the request.\n *\n * @example\n * const tools = toOllamaTools([weatherTool]);\n * await ollama.chat({ model, messages, tools });\n */\nexport function toOllamaTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Ollama wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toParameters(\n input: ToolConfig<unknown, unknown>[\"input\"],\n): Tool[\"function\"][\"parameters\"] {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema as Tool[\"function\"][\"parameters\"];\n }\n\n return { type: \"object\" } as Tool[\"function\"][\"parameters\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;AAOA,SAAS,aACP,OACgC;CAChC,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
|
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: setup-ollama
|
|
11
|
-
description: 'Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. .model({name, vision?, reasoning?}) with cost-truth capabilities — reasoning inferred from thinking-model tags (deepseek-r1/qwq/qwen3/gpt-oss…), promptCaching/audio/pdf honestly false; options.reasoning → Ollama native `think` flag; usage reasoningTokens/cachedTokens stay undefined (none reported). Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`, `reasoning`, `think`, thinking models, `deepseek-r1`, `qwq`, `qwen3`, `gpt-oss`; "use ollama with warlock", "run llama3 locally", "self-hosted llama", "ollama reasoning / thinking models", "deepseek-r1 with warlock"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — the `run-ai-agent` topic of the `warlock-js-ai` skill; provider choice — the `pick-ai-provider` topic of the `warlock-js-ai` skill; embeddings core — the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.'
|
|
11
|
+
description: 'Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. .model({name, vision?, reasoning?}) with cost-truth capabilities — reasoning inferred from thinking-model tags (deepseek-r1/qwq/qwen3/gpt-oss…), promptCaching/audio/pdf honestly false; options.reasoning → Ollama native `think` flag; usage reasoningTokens/cachedTokens stay undefined (none reported). Triggers: `providerOptions`, `OllamaCallOptions`, `countTokens`, `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`, `reasoning`, `think`, thinking models, `streamChunks`, `message.thinking`, `deepseek-r1`, `qwq`, `qwen3`, `gpt-oss`; "use ollama with warlock", "run llama3 locally", "self-hosted llama", "ollama reasoning / thinking models", "deepseek-r1 with warlock"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — the `run-ai-agent` topic of the `warlock-js-ai` skill; provider choice — the `pick-ai-provider` topic of the `warlock-js-ai` skill; embeddings core — the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# `@warlock.js/ai-ollama`
|
|
@@ -20,9 +20,9 @@ Provider adapter that turns a local (or self-hosted) Ollama server into a vendor
|
|
|
20
20
|
```ts
|
|
21
21
|
import { OllamaSDK } from "@warlock.js/ai-ollama";
|
|
22
22
|
|
|
23
|
-
const ollama = new OllamaSDK();
|
|
23
|
+
const ollama = new OllamaSDK(); // local default host
|
|
24
24
|
const remote = new OllamaSDK({ host: "http://gpu-box.internal:11434" });
|
|
25
|
-
const gated
|
|
25
|
+
const gated = new OllamaSDK({
|
|
26
26
|
host: "https://ollama.internal",
|
|
27
27
|
headers: { Authorization: `Bearer ${process.env.OLLAMA_TOKEN}` },
|
|
28
28
|
});
|
|
@@ -33,20 +33,20 @@ const gated = new OllamaSDK({
|
|
|
33
33
|
## Producing a model
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
ollama.model({ name: "llama3.1" })
|
|
37
|
-
ollama.model({ name: "qwen2.5:14b", temperature: 0.2 })
|
|
38
|
-
ollama.model({ name: "llama3.2-vision", maxTokens: 1024 })
|
|
36
|
+
ollama.model({ name: "llama3.1" });
|
|
37
|
+
ollama.model({ name: "qwen2.5:14b", temperature: 0.2 });
|
|
38
|
+
ollama.model({ name: "llama3.2-vision", maxTokens: 1024 });
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Capabilities — what's auto-set
|
|
42
42
|
|
|
43
|
-
| Flag
|
|
44
|
-
|
|
|
45
|
-
| `structuredOutput` | `true` (via Ollama's native `format` JSON-schema field)
|
|
46
|
-
| `vision`
|
|
47
|
-
| `reasoning`
|
|
48
|
-
| `promptCaching`
|
|
49
|
-
| `audio` / `pdf`
|
|
43
|
+
| Flag | Default |
|
|
44
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `structuredOutput` | `true` (via Ollama's native `format` JSON-schema field) |
|
|
46
|
+
| `vision` | Inferred from model tag substring. `true` for `llava`, `bakllava`, `*-vision`, `moondream`, `minicpm-v`, `qwen2-vl`, `qwen2.5-vl`, `llama4`, `gemma3`; `false` otherwise. |
|
|
47
|
+
| `reasoning` | Inferred from model tag substring. `true` for `deepseek-r1`, `qwq`, `qwen3`, `magistral`, `phi4-reasoning`, `phi4-mini-reasoning`, `cogito`, `smallthinker`, `exaone-deep`, `gpt-oss`; `false` otherwise. |
|
|
48
|
+
| `promptCaching` | `false` — Ollama has no provider-side prompt cache. |
|
|
49
|
+
| `audio` / `pdf` | `false` — the Ollama chat API takes no audio / PDF content parts. |
|
|
50
50
|
|
|
51
51
|
Explicit config always wins (`vision`, `structuredOutput`, `reasoning` overrides on `ollama.model({ … })`).
|
|
52
52
|
|
|
@@ -55,9 +55,9 @@ Explicit config always wins (`vision`, `structuredOutput`, `reasoning` overrides
|
|
|
55
55
|
For thinking-capable families the adapter maps `ModelCallOptions.reasoning` onto Ollama's `think` request flag (`boolean | 'low' | 'medium' | 'high'`):
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
|
-
ollama.model({ name: "deepseek-r1:7b" })
|
|
59
|
-
await model.complete(messages, { reasoning: { effort: "high" } });
|
|
60
|
-
await model.complete(messages, { reasoning: { maxTokens: 2048 } });
|
|
58
|
+
ollama.model({ name: "deepseek-r1:7b" });
|
|
59
|
+
await model.complete(messages, { reasoning: { effort: "high" } }); // → chat({ think: "high" })
|
|
60
|
+
await model.complete(messages, { reasoning: { maxTokens: 2048 } }); // → chat({ think: true })
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
- `reasoning.effort` (`low` / `medium` / `high`) passes straight through to `think`.
|
|
@@ -91,7 +91,17 @@ A multipart user message collapses to a single `content` string + an `images` ar
|
|
|
91
91
|
|
|
92
92
|
`model.stream()` drains `chat({ stream: true })` (an `AbortableAsyncIterator`). Each chunk's `message.content` → `{ type: "delta" }`; `message.tool_calls` are emitted as `{ type: "tool-call" }` **fully formed**. Terminal `{ type: "done", finishReason, usage }` — usage from the final (`done: true`) chunk's `prompt_eval_count` / `eval_count`.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Opt-in (`options.streamChunks`, set by `agent.stream()`): `{ type: "reasoning", delta }` per `message.thinking` piece when `think` is on:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
for await (const chunk of model.stream(messages, { reasoning: { effort: "high" }, streamChunks: { reasoning: true } })) {
|
|
98
|
+
if (chunk.type === "reasoning") process.stderr.write(chunk.delta);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Ollama's chat API has no built-in tools: an `ai.providerTool()` for another provider is left out with a warning.
|
|
103
|
+
|
|
104
|
+
**`options.signal` is honored** on `stream()` and `complete()`: a pre-aborted signal rejects without sending, and aborting mid-call cancels the HTTP request. The `ollama` client has no per-request signal, so `OllamaSDK` wraps its `fetch` (yours, when you pass `fetch`) to apply each call's signal from async context; the stream iterator's `abort()` is called too.
|
|
95
105
|
|
|
96
106
|
## Finish-reason mapping
|
|
97
107
|
|
|
@@ -102,7 +112,7 @@ A multipart user message collapses to a single `content` string + an `images` ar
|
|
|
102
112
|
```ts
|
|
103
113
|
const embedder = ollama.embedder({ name: "nomic-embed-text" });
|
|
104
114
|
const { vector } = await embedder.embed("Hello world");
|
|
105
|
-
const { vectors } = await embedder.embedMany(["a", "b"]);
|
|
115
|
+
const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
|
|
106
116
|
const truncated = ollama.embedder({ name: "mxbai-embed-large", dimensions: 512 });
|
|
107
117
|
```
|
|
108
118
|
|
|
@@ -124,14 +134,52 @@ Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. The `ollama` client
|
|
|
124
134
|
## Token counting & usage (cost-truth)
|
|
125
135
|
|
|
126
136
|
```ts
|
|
127
|
-
await ollama.count("some text")
|
|
137
|
+
await ollama.count("some text"); // approximate heuristic, offline
|
|
128
138
|
```
|
|
129
139
|
|
|
130
140
|
Per-call `Usage` comes from the chat response's `prompt_eval_count` (→ `input`) and `eval_count` (→ `output`); `total` is their sum. The adapter reports usage **truthfully** — it never fabricates a field Ollama doesn't return:
|
|
131
141
|
|
|
132
142
|
- `cachedTokens` / `cacheWriteTokens` — **always undefined**. Ollama has no provider prompt cache, so there is no cache-hit or cache-write accounting (honest absence, not a false `0`).
|
|
133
|
-
- `reasoningTokens` — **always undefined**. Reasoning models emit their thinking as the `message.thinking`
|
|
134
|
-
- `cost` —
|
|
143
|
+
- `reasoningTokens` — **always undefined**. Reasoning models emit their thinking as the `message.thinking` _string_, but the wire format carries **no separate reasoning-token count** — those tokens are already folded into `eval_count`. The adapter does not estimate a count from the thinking text.
|
|
144
|
+
- `cost` — attached to each `complete()` / streaming `done` usage only when a `ModelPricing` is configured. The adapter ships no price table (a local model costs nothing per token), so it is usually undefined; set `pricing` for chargeback or a hosted Ollama endpoint.
|
|
145
|
+
- `inputIncludesCache` — left unset: with no cache counters there is nothing to include or exclude.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { totalCost } from "@warlock.js/ai";
|
|
149
|
+
import { OllamaSDK } from "@warlock.js/ai-ollama";
|
|
150
|
+
|
|
151
|
+
const ollama = new OllamaSDK({ pricing: { "llama3.2": { input: 0.05, output: 0.1 } } }); // internal chargeback rate
|
|
152
|
+
const { usage } = await ollama
|
|
153
|
+
.model({ name: "llama3.2" })
|
|
154
|
+
.complete([{ role: "user", content: "hi" }]);
|
|
155
|
+
console.log(totalCost(usage.cost));
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Provider body options
|
|
159
|
+
|
|
160
|
+
Native body fields merge after neutral options. When both values are plain
|
|
161
|
+
objects, their immediate keys merge and caller-provided keys win; untouched
|
|
162
|
+
neutral fields remain. Arrays, scalars and `null` replace the field. This merge
|
|
163
|
+
is one level deep: a nested object explicitly supplied by the caller replaces
|
|
164
|
+
that nested value.
|
|
165
|
+
|
|
166
|
+
`providerOptions.ollama.body` adds native chat request fields after neutral
|
|
167
|
+
options, for completion and streaming:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
await model.complete(messages, {
|
|
171
|
+
providerOptions: { ollama: { body: { keep_alive: "10m", options: { top_k: 4 } } } },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The key stays `ollama` for custom provider labels. Other provider keys are
|
|
176
|
+
ignored. `OllamaCallOptions` is exported. `model`, `messages`, `tools` and `stream`
|
|
177
|
+
are protected; replacing them throws `InvalidRequestError`. Fields merge
|
|
178
|
+
shallowly: a provider `options` object replaces the generated inference options,
|
|
179
|
+
so include any neutral sampling settings you want to retain in that object.
|
|
180
|
+
|
|
181
|
+
Ollama's chat transport has no native preflight token-count endpoint, so its
|
|
182
|
+
model has no `countTokens` method. `sdk.count(text)` remains an offline estimate.
|
|
135
183
|
|
|
136
184
|
## When NOT to use this skill
|
|
137
185
|
|
package/llms.txt
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [setup-ollama](@warlock.js/ai-ollama/setup-ollama/SKILL.md): Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. .model({name, vision?, reasoning?}) with cost-truth capabilities — reasoning inferred from thinking-model tags (deepseek-r1/qwq/qwen3/gpt-oss…), promptCaching/audio/pdf honestly false; options.reasoning → Ollama native `think` flag; usage reasoningTokens/cachedTokens stay undefined (none reported). Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`, `reasoning`, `think`, thinking models, `deepseek-r1`, `qwq`, `qwen3`, `gpt-oss`; "use ollama with warlock", "run llama3 locally", "self-hosted llama", "ollama reasoning / thinking models", "deepseek-r1 with warlock"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — the `run-ai-agent` topic of the `warlock-js-ai` skill; provider choice — the `pick-ai-provider` topic of the `warlock-js-ai` skill; embeddings core — the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.
|
|
9
|
+
- [setup-ollama](@warlock.js/ai-ollama/setup-ollama/SKILL.md): Wire @warlock.js/ai-ollama — new OllamaSDK({host?, headers?}) for local / self-hosted Ollama via the official ollama client (not OpenAI-compat). chat + embed, daemon-down error handling. .model({name, vision?, reasoning?}) with cost-truth capabilities — reasoning inferred from thinking-model tags (deepseek-r1/qwq/qwen3/gpt-oss…), promptCaching/audio/pdf honestly false; options.reasoning → Ollama native `think` flag; usage reasoningTokens/cachedTokens stay undefined (none reported). Triggers: `providerOptions`, `OllamaCallOptions`, `countTokens`, `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`, `reasoning`, `think`, thinking models, `streamChunks`, `message.thinking`, `deepseek-r1`, `qwq`, `qwen3`, `gpt-oss`; "use ollama with warlock", "run llama3 locally", "self-hosted llama", "ollama reasoning / thinking models", "deepseek-r1 with warlock"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — the `run-ai-agent` topic of the `warlock-js-ai` skill; provider choice — the `pick-ai-provider` topic of the `warlock-js-ai` skill; embeddings core — the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-google`; raw `ollama` npm, Vercel `@ai-sdk/ollama`; OpenAI-compat gateway via `@warlock.js/ai-openai` `baseURL`.
|
package/package.json
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"directory": "packages/ai-ollama"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@warlock.js/ai": "5.
|
|
17
|
+
"@warlock.js/ai": "5.32.0"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"ollama": "^0.6.3",
|
|
21
|
-
"@warlock.js/logger": "5.
|
|
21
|
+
"@warlock.js/logger": "5.32.0"
|
|
22
22
|
},
|
|
23
|
-
"version": "5.
|
|
23
|
+
"version": "5.32.0",
|
|
24
24
|
"main": "./cjs/index.cjs",
|
|
25
25
|
"module": "./esm/index.mjs",
|
|
26
26
|
"types": "./esm/index.d.mts",
|