@warlock.js/ai-ollama 5.30.0 → 5.31.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/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
- response = await this.client.chat({
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` by aborting the underlying stream.
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,10 +135,11 @@ var OllamaModel = class {
123
135
  });
124
136
  let stream;
125
137
  try {
126
- stream = await this.client.chat({
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
  }
@@ -139,8 +152,14 @@ var OllamaModel = class {
139
152
  output: 0,
140
153
  total: 0
141
154
  };
155
+ const want = resolveStreamChunks(options);
142
156
  try {
143
157
  for await (const chunk of stream) {
158
+ const thinking = chunk.message?.thinking;
159
+ if (want.reasoning && thinking) yield {
160
+ type: "reasoning",
161
+ delta: thinking
162
+ };
144
163
  const content = chunk.message?.content;
145
164
  if (content) yield {
146
165
  type: "delta",
@@ -166,6 +185,7 @@ var OllamaModel = class {
166
185
  throw this.logAndWrap(thrown);
167
186
  }
168
187
  const finishReason = sawToolCall ? "tool_calls" : mapDoneReason(rawDoneReason);
188
+ attachCost(usage, this.pricing);
169
189
  this.logger.debug(LOG_MODULE, "response", "streaming chat call succeeded", {
170
190
  finishReason,
171
191
  usage
@@ -189,14 +209,14 @@ var OllamaModel = class {
189
209
  ...temperature !== void 0 ? { temperature } : {},
190
210
  ...maxTokens !== void 0 ? { num_predict: maxTokens } : {}
191
211
  };
192
- return {
212
+ return mergeProviderBody({
193
213
  model: this.name,
194
214
  messages: toOllamaMessages(messages),
195
215
  ...Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {},
196
- ...this.buildTools(options?.tools),
216
+ ...this.buildTools(options),
197
217
  ...this.buildFormat(options?.responseSchema),
198
218
  ...this.buildThink(options?.reasoning)
199
- };
219
+ }, options);
200
220
  }
201
221
  /**
202
222
  * Translate the neutral `reasoning` hint into Ollama's `think`
@@ -226,9 +246,15 @@ var OllamaModel = class {
226
246
  * Spread-friendly tools fragment. Empty object when no tools were
227
247
  * supplied so the caller can unconditionally spread it.
228
248
  */
229
- buildTools(tools) {
230
- const mapped = toOllamaTools(tools);
231
- return mapped ? { tools: mapped } : {};
249
+ buildTools(options) {
250
+ const { tools, definitions } = selectProviderTools(options, {
251
+ accepts: [OLLAMA_WIRE_FORMAT, this.provider],
252
+ module: LOG_MODULE,
253
+ model: this.name,
254
+ logger: this.logger
255
+ });
256
+ const mapped = [...toOllamaTools(tools) ?? [], ...definitions];
257
+ return mapped.length > 0 ? { tools: mapped } : {};
232
258
  }
233
259
  /**
234
260
  * 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":";;;;;;;;;;AAyBA,MAAM,aAAa;;AAGnB,MAAM,wBAGF;CACF,SAAS;CACT,KAAK;CACL,QAAQ;CACR,MAAM;CACN,OAAO;CACP,KAAK;AACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAa,cAAb,MAAkD;CAUhD,AAAO,YAAY,QAAgB,QAA2B,WAAmB,UAAU;gBAFzD;EAGhC,KAAK,SAAS;EACd,KAAK,SAAS;EACd,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,UAAU,OAAO;EACtB,KAAK,eAAe;GAClB,kBAAkB,OAAO,oBAAoB;GAC7C,QAAQ,OAAO,UAAU,sBAAsB,OAAO,IAAI;GAI1D,WAAW,OAAO,aAAa,yBAAyB,OAAO,IAAI;GAKnE,eAAe;GACf,OAAO;GACP,KAAK;EACP;CACF;;;;;;;CAQA,MAAa,SAAS,UAAqB,SAAoD;EAC7F,KAAK,OAAO,MAAM,YAAY,WAAW,sBAAsB;GAC7D,OAAO,KAAK;GACZ,cAAc,SAAS;GACvB,WAAW;GACX,WAAW,SAAS,OAAO,UAAU;EACvC,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,WAAW,MAAM,KAAK,OAAO,KAAK;IAAE,GAAG,KAAK,aAAa,UAAU,OAAO;IAAG,QAAQ;GAAM,CAAC;EAC9F,SAAS,QAAQ;GACf,MAAM,KAAK,WAAW,MAAM;EAC9B;EAEA,MAAM,YAAY,KAAK,iBAAiB,SAAS,OAAO;EACxD,MAAM,eAAe,YAAY,eAAe,cAAc,SAAS,WAAW;EAClF,MAAM,QAAQ,KAAK,aAAa,QAAQ;EAExC,KAAK,OAAO,MAAM,YAAY,YAAY,uBAAuB;GAAE;GAAc;EAAM,CAAC;EAExF,OAAO;GACL,SAAS,SAAS,SAAS,WAAW;GACtC;GACA;GACA;EACF;CACF;;;;;;;;CASA,OAAc,OACZ,UACA,SACiC;EACjC,KAAK,OAAO,MAAM,YAAY,WAAW,gCAAgC;GACvE,OAAO,KAAK;GACZ,cAAc,SAAS;GACvB,WAAW;GACX,WAAW,SAAS,OAAO,UAAU;EACvC,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,SAAS,MAAM,KAAK,OAAO,KAAK;IAAE,GAAG,KAAK,aAAa,UAAU,OAAO;IAAG,QAAQ;GAAK,CAAC;EAC3F,SAAS,QAAQ;GACf,MAAM,KAAK,WAAW,MAAM;EAC9B;EAEA,IAAI,SAAS,QACX,IAAI,QAAQ,OAAO,SACjB,OAAO,MAAM;OAEb,QAAQ,OAAO,iBAAiB,eAAe,OAAO,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;EAIjF,IAAI;EACJ,IAAI,cAAc;EAClB,MAAM,QAAe;GAAE,OAAO;GAAG,QAAQ;GAAG,OAAO;EAAE;EAErD,IAAI;GACF,WAAW,MAAM,SAAS,QAAQ;IAChC,MAAM,UAAU,MAAM,SAAS;IAE/B,IAAI,SACF,MAAM;KAAE,MAAM;KAAS;IAAQ;IAGjC,KAAK,MAAM,QAAQ,MAAM,SAAS,cAAc,CAAC,GAAG;KAClD,cAAc;KAEd,MAAM;MACJ,MAAM;MACN,IAAI,KAAK,SAAS;MAClB,MAAM,KAAK,SAAS;MACpB,OAAQ,KAAK,SAAS,aAAa,CAAC;KACtC;IACF;IAEA,IAAI,MAAM,aACR,gBAAgB,MAAM;IAGxB,IAAI,MAAM,MAAM;KACd,MAAM,QAAQ,MAAM,qBAAqB,MAAM;KAC/C,MAAM,SAAS,MAAM,cAAc,MAAM;KACzC,MAAM,QAAQ,MAAM,QAAQ,MAAM;IACpC;GACF;EACF,SAAS,QAAQ;GACf,MAAM,KAAK,WAAW,MAAM;EAC9B;EAEA,MAAM,eAAe,cAAc,eAAe,cAAc,aAAa;EAE7E,KAAK,OAAO,MAAM,YAAY,YAAY,iCAAiC;GACzE;GACA;EACF,CAAC;EAED,MAAM;GAAE,MAAM;GAAQ;GAAc;EAAM;CAC5C;;;;;;;CAQA,AAAQ,aACN,UACA,SAC6B;EAC7B,MAAM,cAAc,SAAS,eAAe,KAAK,OAAO;EACxD,MAAM,YAAY,SAAS,aAAa,KAAK,OAAO;EAEpD,MAAM,gBAAkC;GACtC,GAAI,gBAAgB,SAAY,EAAE,YAAY,IAAI,CAAC;GACnD,GAAI,cAAc,SAAY,EAAE,aAAa,UAAU,IAAI,CAAC;EAC9D;EAEA,OAAO;GACL,OAAO,KAAK;GACZ,UAAU,iBAAiB,QAAQ;GACnC,GAAI,OAAO,KAAK,aAAa,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,cAAc,IAAI,CAAC;GAC1E,GAAG,KAAK,WAAW,SAAS,KAAK;GACjC,GAAG,KAAK,YAAY,SAAS,cAAc;GAC3C,GAAG,KAAK,WAAW,SAAS,SAAS;EACvC;CACF;;;;;;;;;;;;;;;;;;;;CAqBA,AAAQ,WACN,WAC4B;EAC5B,IAAI,CAAC,aAAa,CAAC,KAAK,aAAa,WACnC,OAAO,CAAC;EAKV,IAAI,UAAU,WAAW,QACvB,OAAO,EAAE,OAAO,MAAM;EAGxB,OAAO,EAAE,OAAO,UAAU,SAAS,sBAAsB,UAAU,UAAU,KAAK;CACpF;;;;;CAMA,AAAQ,WAAW,OAA8D;EAC/E,MAAM,SAAS,cAAc,KAAK;EAElC,OAAO,SAAS,EAAE,OAAO,OAAO,IAAI,CAAC;CACvC;;;;;;;;CASA,AAAQ,YACN,gBAC6B;EAC7B,IAAI,CAAC,kBAAkB,CAAC,KAAK,aAAa,kBACxC,OAAO,CAAC;EAGV,IAAI,eAAe,SAAS,YAAY,OAAO,eAAe,eAAe,UAC3E,OAAO,CAAC;EAGV,OAAO,EAAE,QAAQ,eAAe;CAClC;;;;;;;CAQA,AAAQ,iBACN,SACoC;EACpC,MAAM,QAAQ,SAAS;EAEvB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;EAGF,OAAO,MAAM,KAAK,UAAU;GAC1B,IAAI,KAAK,SAAS;GAClB,MAAM,KAAK,SAAS;GACpB,OAAQ,KAAK,SAAS,aAAa,CAAC;EACtC,EAAE;CACJ;;;;;;;;;;;;;;;CAgBA,AAAQ,aAAa,UAA+B;EAClD,MAAM,QAAQ,SAAS,qBAAqB;EAC5C,MAAM,SAAS,SAAS,cAAc;EAEtC,OAAO;GAAE;GAAO;GAAQ,OAAO,QAAQ;EAAO;CAChD;;;;;CAMA,AAAQ,WAAW,QAAiB;EAClC,MAAM,UAAU,gBAAgB,MAAM;EAEtC,KAAK,OAAO,MAAM,YAAY,SAAS,QAAQ,SAAS;GACtD,MAAM,QAAQ;GACd,SAAS,QAAQ;EACnB,CAAC;EAED,OAAO;CACT;AACF"}
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
+ //#region ../../packages/ai-ollama/src/provider-options.d.ts
2
+ /** Adapter-only fields merged after the neutral request parameters. */
3
+ type OllamaCallOptions = {
4
+ body?: Record<string, unknown>;
5
+ };
6
+ //#endregion
7
+ export { OllamaCallOptions };
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":";;KAGY,iBAAA;EAAsB,IAAA,GAAO,MAAM;AAAA"}
@@ -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 = 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,kBAAkB,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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,GAAQ,eAAA;EAgCwB;;;;;;AAWY;;;EA1BxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
1
+ {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,GAAQ,eAAA;EAsCwB;;;;;;AAWY;;;EA1BxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
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(clientConfig);
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 {\n OllamaEmbedderConfig,\n OllamaModelConfig,\n OllamaSDKConfig,\n} from \"./config.type\";\nimport { OllamaEmbedder } from \"./embedder\";\nimport { OllamaModel } from \"./model\";\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 this.client = new Ollama(clientConfig);\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,SAA0B,CAAC,GAAG;EAC/C,MAAM,EAAE,UAAU,SAAS,GAAG,iBAAiB;EAE/C,KAAK,SAAS,IAAI,OAAO,YAAY;EACrC,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"}
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"}
@@ -1,3 +1,4 @@
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";
@@ -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,aAAa,OAA8E;CAClG,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
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(); // local default host
23
+ const ollama = new OllamaSDK(); // local default host
24
24
  const remote = new OllamaSDK({ host: "http://gpu-box.internal:11434" });
25
- const gated = new OllamaSDK({
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 | 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. |
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" } }); // → chat({ think: "high" })
60
- await model.complete(messages, { reasoning: { maxTokens: 2048 } }); // → chat({ think: true })
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
- **`options.signal` is honored** by calling the iterator's `abort()` (stream path; non-stream `complete()` ignores it — the agent still honors the signal at trip boundaries).
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"]); // single batched call
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") // approximate heuristic, offline
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` *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.
134
- - `cost` — populated only when a `ModelPricing` is configured (local Ollama is free, so usually undefined).
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.30.0"
17
+ "@warlock.js/ai": "5.31.0"
18
18
  },
19
19
  "dependencies": {
20
20
  "ollama": "^0.6.3",
21
- "@warlock.js/logger": "5.30.0"
21
+ "@warlock.js/logger": "5.31.0"
22
22
  },
23
- "version": "5.30.0",
23
+ "version": "5.31.0",
24
24
  "main": "./cjs/index.cjs",
25
25
  "module": "./esm/index.mjs",
26
26
  "types": "./esm/index.d.mts",