@warlock.js/ai-ollama 4.2.11 → 4.3.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 CHANGED
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 4.3.0 - 2026-06-21
10
+
11
+ ### Added
12
+
13
+ - Cost-truth contract wiring. `ModelCapabilities` now reports `reasoning` (inferred from thinking-capable model tags — `deepseek-r1`, `qwq`, `qwen3`, `magistral`, `phi4-reasoning`, `cogito`, `smallthinker`, `exaone-deep`, `gpt-oss`; overridable via `ollama.model({ name, reasoning })`), plus `promptCaching`, `audio`, and `pdf` reported truthfully as `false` (Ollama supports none).
14
+ - `ModelCallOptions.reasoning` maps onto Ollama's native `think` request flag: `reasoning.effort` (`low`/`medium`/`high`) passes straight through, and an effort-less hint becomes `think: true`. The flag is sent only to `reasoning`-capable models; `reasoning.maxTokens` and `cacheControl` are honored as graceful no-ops (Ollama exposes no thinking-budget cap and no prompt cache).
15
+
16
+ ### Notes
17
+
18
+ - `Usage` stays honest: Ollama reports no prompt-cache or reasoning-token counts, so `cachedTokens` / `cacheWriteTokens` / `reasoningTokens` remain `undefined` (reasoning tokens are folded into `eval_count`; the adapter does not fabricate a count from the `message.thinking` text).
19
+
9
20
  ## 4.1.15
10
21
 
11
22
  - Baseline — per-package changelog tracking starts at this version.
package/cjs/index.cjs CHANGED
@@ -346,6 +346,51 @@ var OllamaEmbedder = class {
346
346
  }
347
347
  };
348
348
 
349
+ //#endregion
350
+ //#region ../@warlock.js/ai-ollama/src/known-reasoning-models.ts
351
+ /**
352
+ * Substrings identifying Ollama model tags whose family emits a
353
+ * reasoning / "thinking" channel before the visible answer.
354
+ *
355
+ * Ollama exposes thinking via the request-side `think` flag and the
356
+ * response-side `message.thinking` string. Only models trained for it
357
+ * honor `think`; sending it to a non-reasoning model is a no-op at best.
358
+ * Tags are family-named with optional size/quant suffixes
359
+ * (`deepseek-r1:7b`, `qwq:32b-preview`), so a substring match tolerates
360
+ * the suffixes. Covers the common reasoning families on the Ollama
361
+ * registry; plain instruct models (`llama3.1`, `mistral`, `phi3`) are
362
+ * excluded. Override per-model via
363
+ * `ollama.model({ name, reasoning: true | false })`.
364
+ */
365
+ const REASONING_CAPABLE_SUBSTRINGS = [
366
+ "deepseek-r1",
367
+ "qwq",
368
+ "qwen3",
369
+ "magistral",
370
+ "phi4-reasoning",
371
+ "phi4-mini-reasoning",
372
+ "cogito",
373
+ "smallthinker",
374
+ "exaone-deep",
375
+ "gpt-oss"
376
+ ];
377
+ /**
378
+ * Infer whether an Ollama model tag supports a reasoning / thinking
379
+ * channel based on the known thinking-family substrings. Unknown tags
380
+ * default to `false` so the adapter never sends the `think` flag to a
381
+ * model that cannot honor it (a no-op for plain instruct models).
382
+ *
383
+ * @example
384
+ * inferReasoningCapability("deepseek-r1:7b"); // → true
385
+ * inferReasoningCapability("qwq:32b"); // → true
386
+ * inferReasoningCapability("llama3.1"); // → false
387
+ * inferReasoningCapability("nomic-embed-text"); // → false
388
+ */
389
+ function inferReasoningCapability(modelName) {
390
+ const normalized = modelName.toLowerCase();
391
+ return REASONING_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));
392
+ }
393
+
349
394
  //#endregion
350
395
  //#region ../@warlock.js/ai-ollama/src/known-vision-models.ts
351
396
  /**
@@ -437,7 +482,11 @@ var OllamaModel = class {
437
482
  this.pricing = config.pricing;
438
483
  this.capabilities = {
439
484
  structuredOutput: config.structuredOutput ?? true,
440
- vision: config.vision ?? inferVisionCapability(config.name)
485
+ vision: config.vision ?? inferVisionCapability(config.name),
486
+ reasoning: config.reasoning ?? inferReasoningCapability(config.name),
487
+ promptCaching: false,
488
+ audio: false,
489
+ pdf: false
441
490
  };
442
491
  }
443
492
  /**
@@ -563,10 +612,31 @@ var OllamaModel = class {
563
612
  messages: toOllamaMessages(messages),
564
613
  ...Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {},
565
614
  ...this.buildTools(options?.tools),
566
- ...this.buildFormat(options?.responseSchema)
615
+ ...this.buildFormat(options?.responseSchema),
616
+ ...this.buildThink(options?.reasoning)
567
617
  };
568
618
  }
569
619
  /**
620
+ * Translate the neutral `reasoning` hint into Ollama's `think`
621
+ * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'
622
+ * | 'high'`, so the neutral `ReasoningEffort` literals pass straight
623
+ * through; an effort-less `reasoning` (only `maxTokens`, or an empty
624
+ * object) becomes `think: true` to switch the channel on.
625
+ *
626
+ * No-ops unless the model is reasoning-capable, so the `think` flag is
627
+ * never sent to a plain instruct model that cannot honor it.
628
+ *
629
+ * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama
630
+ * equivalent — the daemon does not accept a thinking-token cap — so it
631
+ * is honored only as the on/off signal above and otherwise ignored.
632
+ * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no
633
+ * provider prompt cache, so there is no cache breakpoint to place.
634
+ */
635
+ buildThink(reasoning) {
636
+ if (!reasoning || !this.capabilities.reasoning) return {};
637
+ return { think: reasoning.effort ?? true };
638
+ }
639
+ /**
570
640
  * Spread-friendly tools fragment. Empty object when no tools were
571
641
  * supplied so the caller can unconditionally spread it.
572
642
  */
@@ -603,8 +673,17 @@ var OllamaModel = class {
603
673
  }
604
674
  /**
605
675
  * Normalize Ollama's eval counts into the neutral `Usage` shape.
606
- * Ollama runs locally with no prompt cache, so there is no
607
- * `cachedTokens`; `total` is computed from input + output.
676
+ *
677
+ * Cost-truth: Ollama reports only `prompt_eval_count` (input) and
678
+ * `eval_count` (output). It has **no** provider prompt cache, so
679
+ * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined
680
+ * (honest absence, not a false zero). Reasoning models emit their
681
+ * thinking as the `message.thinking` *string* but the wire format
682
+ * carries **no separate reasoning-token count** — the thinking tokens
683
+ * are already folded into `eval_count`. We therefore do not fabricate
684
+ * a `Usage.reasoningTokens` from the text length; it is left undefined
685
+ * unless/until the daemon exposes a real count. `total` is input +
686
+ * output.
608
687
  */
609
688
  extractUsage(response) {
610
689
  const input = response.prompt_eval_count ?? 0;
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["InvalidRequestError","AIError","ProviderTimeoutError","ProviderError","ProviderAuthError","ProviderRateLimitError","ContextLengthExceededError","InvalidRequestError","LOG_MODULE","log","log","Ollama"],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/map-done-reason.ts","../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-messages.ts","../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-tools.ts","../../../../../../@warlock.js/ai-ollama/src/utils/wrap-ollama-error.ts","../../../../../../@warlock.js/ai-ollama/src/embedder.ts","../../../../../../@warlock.js/ai-ollama/src/known-vision-models.ts","../../../../../../@warlock.js/ai-ollama/src/model.ts","../../../../../../@warlock.js/ai-ollama/src/sdk.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst doneReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n length: \"length\",\n};\n\n/**\n * Map Ollama's `done_reason` to the normalized `FinishReason` union.\n *\n * `stop` is the natural terminal; `length` means the `num_predict`\n * cap was hit. Anything else — `load` (model load only, no\n * generation), an empty string, or any future value — falls through\n * to `\"error\"`.\n *\n * Note: Ollama has no tool-use done reason — it sets `done_reason:\n * \"stop\"` and populates `message.tool_calls`. `OllamaModel` derives\n * `\"tool_calls\"` from tool-call presence; this map stays purely about\n * the raw signal.\n *\n * @example\n * mapDoneReason(\"stop\"); // \"stop\"\n * mapDoneReason(\"length\"); // \"length\"\n * mapDoneReason(\"load\"); // \"error\"\n * mapDoneReason(undefined); // \"error\"\n */\nexport function mapDoneReason(raw: string | null | undefined): FinishReason {\n return doneReasonMap[raw ?? \"\"] ?? \"error\";\n}\n","import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type { Message as OllamaMessage } from \"ollama\";\n\n/**\n * Convert vendor-neutral `Message[]` into the Ollama chat message\n * shape.\n *\n * Unlike Anthropic / Gemini / Bedrock, Ollama keeps a first-class\n * `system` role inside `messages`, so there is no system-prompt\n * hoisting — roles pass straight through. The Ollama specifics this\n * absorbs:\n *\n * 1. **Tool calls.** An assistant message with `toolCalls` becomes an\n * `assistant` message whose `tool_calls` is the Ollama\n * `{ function: { name, arguments } }` shape (Ollama has no tool-call\n * id — see `OllamaModel`/decisions for the synthesized-id note).\n * 2. **Tool results.** A neutral `tool` message becomes a `tool`\n * message with `tool_name` set from `toolCallId` (Ollama matches a\n * result to its call by tool name).\n * 3. **Images.** Multipart user content collapses to a single\n * `content` string plus an `images` array of base64 strings.\n *\n * @example\n * const messages = toOllamaMessages([\n * { role: \"system\", content: \"Be concise.\" },\n * { role: \"user\", content: \"Hi\" },\n * ]);\n */\nexport function toOllamaMessages(messages: Message[]): OllamaMessage[] {\n return messages.map((message): OllamaMessage => {\n if (message.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(message.content),\n tool_name: message.toolCallId ?? \"\",\n };\n }\n\n if (message.role === \"assistant\" && message.toolCalls && message.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(message.content),\n tool_calls: message.toolCalls.map((toolCall) => ({\n function: {\n name: toolCall.name,\n arguments: (toolCall.input ?? {}) as Record<string, unknown>,\n },\n })),\n };\n }\n\n if (message.role === \"user\" && Array.isArray(message.content)) {\n return toMultipartMessage(message.content);\n }\n\n return { role: message.role, content: stringifyContent(message.content) };\n });\n}\n\n/**\n * Collapse a `ContentPart[]` user message into Ollama's\n * single-string-content + base64-`images` shape. Ollama cannot fetch\n * remote URLs, so a `{ url }` image surfaces a typed\n * `InvalidRequestError` upfront (consistent with the Bedrock/Gemini\n * adapters). The agent has already resolved attachments — nothing is\n * fetched here.\n */\nfunction toMultipartMessage(parts: ContentPart[]): OllamaMessage {\n const textChunks: string[] = [];\n const images: string[] = [];\n\n for (const part of parts) {\n if (part.type === \"text\") {\n textChunks.push(part.text);\n\n continue;\n }\n\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"Ollama does not fetch remote-URL images; supply base64 image bytes instead.\",\n );\n }\n\n images.push(part.source.base64);\n }\n\n return {\n role: \"user\",\n content: textChunks.join(\"\"),\n ...(images.length > 0 ? { images } : {}),\n };\n}\n\n/**\n * Multipart content on a non-user role collapses to concatenated text;\n * plain strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n","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","import {\n AIError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n} from \"@warlock.js/ai\";\n\n/**\n * Raw-error fields the wrapper reads off an Ollama client error. The\n * `ollama` client throws a `ResponseError` (`name: \"ResponseError\"`,\n * numeric `status_code`, message = the server's `error` text) for HTTP\n * faults; transport failures surface as a `fetch`-layer `TypeError`\n * with an `ECONNREFUSED` / `ETIMEDOUT` cause. We duck-type both —\n * `ResponseError` is internal to the package and not exported.\n */\ntype OllamaErrorShape = {\n name?: string;\n message?: string;\n statusCode?: number;\n code?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the Ollama adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** HTTP faults carry `status_code`; the local\n * daemon being down surfaces as a connection error (`ECONNREFUSED` /\n * \"fetch failed\") — mapped to `ProviderError` since it's an\n * operational \"is Ollama running?\" condition, not a request defect.\n * `400` with context-length phrasing maps to\n * `ContextLengthExceededError`.\n *\n * `AIError` instances pass through unchanged so `catch/throw wrap(e)`\n * pipelines never double-wrap.\n *\n * @example\n * try {\n * return await this.client.chat({ ... });\n * } catch (thrown) {\n * throw wrapOllamaError(thrown);\n * }\n */\nexport function wrapOllamaError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (isConnectionRefused(shape, message)) {\n return new ProviderError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 401 || shape.statusCode === 403) {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 429) {\n return new ProviderRateLimitError(message, { cause: thrown, context });\n }\n\n if (isClientStatus(shape.statusCode)) {\n if (/context length|too long|exceeds|maximum context/i.test(message)) {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape. `ResponseError` exposes `status_code`;\n * fetch-layer errors carry a `cause` whose `code` is the OS-level\n * socket error.\n */\nfunction toShape(thrown: unknown): OllamaErrorShape {\n if (typeof thrown !== \"object\" || thrown === null) {\n return {};\n }\n\n const raw = thrown as Record<string, unknown>;\n const cause = raw.cause as Record<string, unknown> | undefined;\n\n return {\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n statusCode: typeof raw.status_code === \"number\" ? raw.status_code : undefined,\n code:\n typeof raw.code === \"string\"\n ? raw.code\n : cause && typeof cause.code === \"string\"\n ? cause.code\n : undefined,\n };\n}\n\n/** Transport-level timeout signals. */\nfunction isTimeout(shape: OllamaErrorShape): boolean {\n if (shape.name === \"AbortError\" || shape.name === \"TimeoutError\") {\n return true;\n }\n\n return shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\";\n}\n\n/**\n * The Ollama daemon not being reachable (most common local failure):\n * connection refused at the socket layer, or the `fetch failed`\n * TypeError the client surfaces when the host is down.\n */\nfunction isConnectionRefused(shape: OllamaErrorShape, message: string): boolean {\n return shape.code === \"ECONNREFUSED\" || /fetch failed|econnrefused/i.test(message);\n}\n\n/** True for HTTP 4xx — a client-side request problem, not a server fault. */\nfunction isClientStatus(status: number | undefined): boolean {\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/** Attach the diagnostic fields to `error.context`. */\nfunction buildContext(shape: OllamaErrorShape): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.statusCode !== undefined) {\n context.status = shape.statusCode;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n return context;\n}\n","import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type { EmbedResponse, Ollama } from \"ollama\";\nimport type { OllamaEmbedderConfig } from \"./config.type\";\nimport { wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/**\n * Ollama-backed implementation of `EmbedderContract`\n * (`nomic-embed-text`, `mxbai-embed-large`, …) via `client.embed`.\n *\n * **Role.** Converts text into floating-point vectors. Standalone\n * primitive — unrelated to chat / tools / the agent loop.\n *\n * **Batch is native.** Ollama's `embed` accepts a string array and\n * returns `embeddings` in input order, so `embedMany` is a single\n * request (like the Gemini adapter, unlike Bedrock/Titan).\n *\n * **Usage.** Ollama returns only `prompt_eval_count` (no separate\n * total); it is reported as both `promptTokens` and `totalTokens`.\n *\n * **Dimensions.** When no `dimensions` override is given,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached. Passing `dimensions`\n * forwards Ollama's truncation field and sets the initial value.\n *\n * @example\n * const embedder = new OllamaEmbedder(client, { name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class OllamaEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider: string;\n public dimensions: number;\n\n private readonly client: Ollama;\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(\n client: Ollama,\n config: OllamaEmbedderConfig,\n provider: string = \"ollama\",\n ) {\n this.client = client;\n this.name = config.name;\n this.provider = provider;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const { embeddings, usage } = await this.request([input]);\n\n return { vector: embeddings[0] ?? [], dimensions: this.dimensions, usage };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const { embeddings, usage } = await this.request(inputs);\n\n return { vectors: embeddings, dimensions: this.dimensions, usage };\n }\n\n /**\n * Shared transport: one `embed` call for the whole batch, wrap\n * provider errors, cache `dimensions` from the first vector, and\n * return vectors in input order plus a neutral usage object.\n */\n private async request(\n inputs: string[],\n ): Promise<{ embeddings: number[][]; usage: EmbeddingUsage }> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embed\", {\n model: this.name,\n count: inputs.length,\n });\n\n let response: EmbedResponse;\n\n try {\n response = await this.client.embed({\n model: this.name,\n input: inputs,\n ...(this.configuredDimensions !== undefined\n ? { dimensions: this.configuredDimensions }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const embeddings = response.embeddings ?? [];\n\n if (this.dimensions === 0 && embeddings[0]) {\n this.dimensions = embeddings[0].length;\n }\n\n const tokens = response.prompt_eval_count ?? 0;\n const usage: EmbeddingUsage = { promptTokens: tokens, totalTokens: tokens };\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embed returned\", {\n count: embeddings.length,\n dimensions: this.dimensions,\n });\n\n return { embeddings, usage };\n }\n}\n","/**\n * Substrings identifying Ollama model tags whose family accepts image\n * input (vision).\n *\n * Ollama tags are family-named with optional size/quant suffixes\n * (`llama3.2-vision:11b`, `llava:13b-v1.6`, `qwen2.5-vl:7b`). A\n * substring match tolerates those suffixes. Covers the common\n * multimodal families on the Ollama registry; text-only models\n * (`llama3.1`, `mistral`, `phi3`, `nomic-embed-text`) are excluded.\n * Override per-model via `ollama.model({ name, vision: true | false })`.\n */\nconst VISION_CAPABLE_SUBSTRINGS = [\n \"llava\",\n \"vision\",\n \"bakllava\",\n \"moondream\",\n \"minicpm-v\",\n \"qwen2-vl\",\n \"qwen2.5-vl\",\n \"llama4\",\n \"gemma3\",\n];\n\n/**\n * Infer whether an Ollama model tag supports vision based on the known\n * multimodal-family substrings. Unknown tags default to `false` so\n * passing an image to a text-only local model surfaces a clear,\n * agent-side capability error instead of the image being silently\n * ignored by the model.\n *\n * @example\n * inferVisionCapability(\"llama3.2-vision:11b\"); // → true\n * inferVisionCapability(\"llava:13b\"); // → true\n * inferVisionCapability(\"llama3.1\"); // → false\n * inferVisionCapability(\"nomic-embed-text\"); // → false\n */\nexport function inferVisionCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n","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 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 { inferVisionCapability } from \"./known-vision-models\";\nimport { mapDoneReason, toOllamaMessages, toOllamaTools, wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\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 };\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 };\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 * Ollama runs locally with no prompt cache, so there is no\n * `cachedTokens`; `total` is computed from input + 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","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":"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"}
1
+ {"version":3,"file":"index.cjs","names":["InvalidRequestError","AIError","ProviderTimeoutError","ProviderError","ProviderAuthError","ProviderRateLimitError","ContextLengthExceededError","InvalidRequestError","LOG_MODULE","log","log","Ollama"],"sources":["../../../../../../@warlock.js/ai-ollama/src/utils/map-done-reason.ts","../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-messages.ts","../../../../../../@warlock.js/ai-ollama/src/utils/to-ollama-tools.ts","../../../../../../@warlock.js/ai-ollama/src/utils/wrap-ollama-error.ts","../../../../../../@warlock.js/ai-ollama/src/embedder.ts","../../../../../../@warlock.js/ai-ollama/src/known-reasoning-models.ts","../../../../../../@warlock.js/ai-ollama/src/known-vision-models.ts","../../../../../../@warlock.js/ai-ollama/src/model.ts","../../../../../../@warlock.js/ai-ollama/src/sdk.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst doneReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n length: \"length\",\n};\n\n/**\n * Map Ollama's `done_reason` to the normalized `FinishReason` union.\n *\n * `stop` is the natural terminal; `length` means the `num_predict`\n * cap was hit. Anything else — `load` (model load only, no\n * generation), an empty string, or any future value — falls through\n * to `\"error\"`.\n *\n * Note: Ollama has no tool-use done reason — it sets `done_reason:\n * \"stop\"` and populates `message.tool_calls`. `OllamaModel` derives\n * `\"tool_calls\"` from tool-call presence; this map stays purely about\n * the raw signal.\n *\n * @example\n * mapDoneReason(\"stop\"); // \"stop\"\n * mapDoneReason(\"length\"); // \"length\"\n * mapDoneReason(\"load\"); // \"error\"\n * mapDoneReason(undefined); // \"error\"\n */\nexport function mapDoneReason(raw: string | null | undefined): FinishReason {\n return doneReasonMap[raw ?? \"\"] ?? \"error\";\n}\n","import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type { Message as OllamaMessage } from \"ollama\";\n\n/**\n * Convert vendor-neutral `Message[]` into the Ollama chat message\n * shape.\n *\n * Unlike Anthropic / Gemini / Bedrock, Ollama keeps a first-class\n * `system` role inside `messages`, so there is no system-prompt\n * hoisting — roles pass straight through. The Ollama specifics this\n * absorbs:\n *\n * 1. **Tool calls.** An assistant message with `toolCalls` becomes an\n * `assistant` message whose `tool_calls` is the Ollama\n * `{ function: { name, arguments } }` shape (Ollama has no tool-call\n * id — see `OllamaModel`/decisions for the synthesized-id note).\n * 2. **Tool results.** A neutral `tool` message becomes a `tool`\n * message with `tool_name` set from `toolCallId` (Ollama matches a\n * result to its call by tool name).\n * 3. **Images.** Multipart user content collapses to a single\n * `content` string plus an `images` array of base64 strings.\n *\n * @example\n * const messages = toOllamaMessages([\n * { role: \"system\", content: \"Be concise.\" },\n * { role: \"user\", content: \"Hi\" },\n * ]);\n */\nexport function toOllamaMessages(messages: Message[]): OllamaMessage[] {\n return messages.map((message): OllamaMessage => {\n if (message.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(message.content),\n tool_name: message.toolCallId ?? \"\",\n };\n }\n\n if (message.role === \"assistant\" && message.toolCalls && message.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(message.content),\n tool_calls: message.toolCalls.map((toolCall) => ({\n function: {\n name: toolCall.name,\n arguments: (toolCall.input ?? {}) as Record<string, unknown>,\n },\n })),\n };\n }\n\n if (message.role === \"user\" && Array.isArray(message.content)) {\n return toMultipartMessage(message.content);\n }\n\n return { role: message.role, content: stringifyContent(message.content) };\n });\n}\n\n/**\n * Collapse a `ContentPart[]` user message into Ollama's\n * single-string-content + base64-`images` shape. Ollama cannot fetch\n * remote URLs, so a `{ url }` image surfaces a typed\n * `InvalidRequestError` upfront (consistent with the Bedrock/Gemini\n * adapters). The agent has already resolved attachments — nothing is\n * fetched here.\n */\nfunction toMultipartMessage(parts: ContentPart[]): OllamaMessage {\n const textChunks: string[] = [];\n const images: string[] = [];\n\n for (const part of parts) {\n if (part.type === \"text\") {\n textChunks.push(part.text);\n\n continue;\n }\n\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"Ollama does not fetch remote-URL images; supply base64 image bytes instead.\",\n );\n }\n\n images.push(part.source.base64);\n }\n\n return {\n role: \"user\",\n content: textChunks.join(\"\"),\n ...(images.length > 0 ? { images } : {}),\n };\n}\n\n/**\n * Multipart content on a non-user role collapses to concatenated text;\n * plain strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n","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","import {\n AIError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n} from \"@warlock.js/ai\";\n\n/**\n * Raw-error fields the wrapper reads off an Ollama client error. The\n * `ollama` client throws a `ResponseError` (`name: \"ResponseError\"`,\n * numeric `status_code`, message = the server's `error` text) for HTTP\n * faults; transport failures surface as a `fetch`-layer `TypeError`\n * with an `ECONNREFUSED` / `ETIMEDOUT` cause. We duck-type both —\n * `ResponseError` is internal to the package and not exported.\n */\ntype OllamaErrorShape = {\n name?: string;\n message?: string;\n statusCode?: number;\n code?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the Ollama adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** HTTP faults carry `status_code`; the local\n * daemon being down surfaces as a connection error (`ECONNREFUSED` /\n * \"fetch failed\") — mapped to `ProviderError` since it's an\n * operational \"is Ollama running?\" condition, not a request defect.\n * `400` with context-length phrasing maps to\n * `ContextLengthExceededError`.\n *\n * `AIError` instances pass through unchanged so `catch/throw wrap(e)`\n * pipelines never double-wrap.\n *\n * @example\n * try {\n * return await this.client.chat({ ... });\n * } catch (thrown) {\n * throw wrapOllamaError(thrown);\n * }\n */\nexport function wrapOllamaError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (isConnectionRefused(shape, message)) {\n return new ProviderError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 401 || shape.statusCode === 403) {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.statusCode === 429) {\n return new ProviderRateLimitError(message, { cause: thrown, context });\n }\n\n if (isClientStatus(shape.statusCode)) {\n if (/context length|too long|exceeds|maximum context/i.test(message)) {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape. `ResponseError` exposes `status_code`;\n * fetch-layer errors carry a `cause` whose `code` is the OS-level\n * socket error.\n */\nfunction toShape(thrown: unknown): OllamaErrorShape {\n if (typeof thrown !== \"object\" || thrown === null) {\n return {};\n }\n\n const raw = thrown as Record<string, unknown>;\n const cause = raw.cause as Record<string, unknown> | undefined;\n\n return {\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n statusCode: typeof raw.status_code === \"number\" ? raw.status_code : undefined,\n code:\n typeof raw.code === \"string\"\n ? raw.code\n : cause && typeof cause.code === \"string\"\n ? cause.code\n : undefined,\n };\n}\n\n/** Transport-level timeout signals. */\nfunction isTimeout(shape: OllamaErrorShape): boolean {\n if (shape.name === \"AbortError\" || shape.name === \"TimeoutError\") {\n return true;\n }\n\n return shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\";\n}\n\n/**\n * The Ollama daemon not being reachable (most common local failure):\n * connection refused at the socket layer, or the `fetch failed`\n * TypeError the client surfaces when the host is down.\n */\nfunction isConnectionRefused(shape: OllamaErrorShape, message: string): boolean {\n return shape.code === \"ECONNREFUSED\" || /fetch failed|econnrefused/i.test(message);\n}\n\n/** True for HTTP 4xx — a client-side request problem, not a server fault. */\nfunction isClientStatus(status: number | undefined): boolean {\n return typeof status === \"number\" && status >= 400 && status < 500;\n}\n\n/** Attach the diagnostic fields to `error.context`. */\nfunction buildContext(shape: OllamaErrorShape): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.statusCode !== undefined) {\n context.status = shape.statusCode;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n return context;\n}\n","import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type { EmbedResponse, Ollama } from \"ollama\";\nimport type { OllamaEmbedderConfig } from \"./config.type\";\nimport { wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/**\n * Ollama-backed implementation of `EmbedderContract`\n * (`nomic-embed-text`, `mxbai-embed-large`, …) via `client.embed`.\n *\n * **Role.** Converts text into floating-point vectors. Standalone\n * primitive — unrelated to chat / tools / the agent loop.\n *\n * **Batch is native.** Ollama's `embed` accepts a string array and\n * returns `embeddings` in input order, so `embedMany` is a single\n * request (like the Gemini adapter, unlike Bedrock/Titan).\n *\n * **Usage.** Ollama returns only `prompt_eval_count` (no separate\n * total); it is reported as both `promptTokens` and `totalTokens`.\n *\n * **Dimensions.** When no `dimensions` override is given,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached. Passing `dimensions`\n * forwards Ollama's truncation field and sets the initial value.\n *\n * @example\n * const embedder = new OllamaEmbedder(client, { name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class OllamaEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider: string;\n public dimensions: number;\n\n private readonly client: Ollama;\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(\n client: Ollama,\n config: OllamaEmbedderConfig,\n provider: string = \"ollama\",\n ) {\n this.client = client;\n this.name = config.name;\n this.provider = provider;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const { embeddings, usage } = await this.request([input]);\n\n return { vector: embeddings[0] ?? [], dimensions: this.dimensions, usage };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const { embeddings, usage } = await this.request(inputs);\n\n return { vectors: embeddings, dimensions: this.dimensions, usage };\n }\n\n /**\n * Shared transport: one `embed` call for the whole batch, wrap\n * provider errors, cache `dimensions` from the first vector, and\n * return vectors in input order plus a neutral usage object.\n */\n private async request(\n inputs: string[],\n ): Promise<{ embeddings: number[][]; usage: EmbeddingUsage }> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embed\", {\n model: this.name,\n count: inputs.length,\n });\n\n let response: EmbedResponse;\n\n try {\n response = await this.client.embed({\n model: this.name,\n input: inputs,\n ...(this.configuredDimensions !== undefined\n ? { dimensions: this.configuredDimensions }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const embeddings = response.embeddings ?? [];\n\n if (this.dimensions === 0 && embeddings[0]) {\n this.dimensions = embeddings[0].length;\n }\n\n const tokens = response.prompt_eval_count ?? 0;\n const usage: EmbeddingUsage = { promptTokens: tokens, totalTokens: tokens };\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embed returned\", {\n count: embeddings.length,\n dimensions: this.dimensions,\n });\n\n return { embeddings, usage };\n }\n}\n","/**\n * Substrings identifying Ollama model tags whose family emits a\n * reasoning / \"thinking\" channel before the visible answer.\n *\n * Ollama exposes thinking via the request-side `think` flag and the\n * response-side `message.thinking` string. Only models trained for it\n * honor `think`; sending it to a non-reasoning model is a no-op at best.\n * Tags are family-named with optional size/quant suffixes\n * (`deepseek-r1:7b`, `qwq:32b-preview`), so a substring match tolerates\n * the suffixes. Covers the common reasoning families on the Ollama\n * registry; plain instruct models (`llama3.1`, `mistral`, `phi3`) are\n * excluded. Override per-model via\n * `ollama.model({ name, reasoning: true | false })`.\n */\nconst REASONING_CAPABLE_SUBSTRINGS = [\n \"deepseek-r1\",\n \"qwq\",\n \"qwen3\",\n \"magistral\",\n \"phi4-reasoning\",\n \"phi4-mini-reasoning\",\n \"cogito\",\n \"smallthinker\",\n \"exaone-deep\",\n \"gpt-oss\",\n];\n\n/**\n * Infer whether an Ollama model tag supports a reasoning / thinking\n * channel based on the known thinking-family substrings. Unknown tags\n * default to `false` so the adapter never sends the `think` flag to a\n * model that cannot honor it (a no-op for plain instruct models).\n *\n * @example\n * inferReasoningCapability(\"deepseek-r1:7b\"); // → true\n * inferReasoningCapability(\"qwq:32b\"); // → true\n * inferReasoningCapability(\"llama3.1\"); // → false\n * inferReasoningCapability(\"nomic-embed-text\"); // → false\n */\nexport function inferReasoningCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return REASONING_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n","/**\n * Substrings identifying Ollama model tags whose family accepts image\n * input (vision).\n *\n * Ollama tags are family-named with optional size/quant suffixes\n * (`llama3.2-vision:11b`, `llava:13b-v1.6`, `qwen2.5-vl:7b`). A\n * substring match tolerates those suffixes. Covers the common\n * multimodal families on the Ollama registry; text-only models\n * (`llama3.1`, `mistral`, `phi3`, `nomic-embed-text`) are excluded.\n * Override per-model via `ollama.model({ name, vision: true | false })`.\n */\nconst VISION_CAPABLE_SUBSTRINGS = [\n \"llava\",\n \"vision\",\n \"bakllava\",\n \"moondream\",\n \"minicpm-v\",\n \"qwen2-vl\",\n \"qwen2.5-vl\",\n \"llama4\",\n \"gemma3\",\n];\n\n/**\n * Infer whether an Ollama model tag supports vision based on the known\n * multimodal-family substrings. Unknown tags default to `false` so\n * passing an image to a text-only local model surfaces a clear,\n * agent-side capability error instead of the image being silently\n * ignored by the model.\n *\n * @example\n * inferVisionCapability(\"llama3.2-vision:11b\"); // → true\n * inferVisionCapability(\"llava:13b\"); // → true\n * inferVisionCapability(\"llama3.1\"); // → false\n * inferVisionCapability(\"nomic-embed-text\"); // → false\n */\nexport function inferVisionCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n","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 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/**\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 the neutral `ReasoningEffort` literals pass straight\n * through; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\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 return { think: 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","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":"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"}
@@ -64,6 +64,16 @@ type OllamaModelConfig = ModelConfig & {
64
64
  * re-injects a soft schema hint into the system prompt instead.
65
65
  */
66
66
  structuredOutput?: boolean;
67
+ /**
68
+ * Override the auto-inferred reasoning / thinking capability. When
69
+ * omitted, the adapter checks the model tag against the known
70
+ * thinking-capable Ollama families (see `known-reasoning-models.ts`).
71
+ * When `true`, the adapter maps `ModelCallOptions.reasoning` onto
72
+ * Ollama's `think` request flag; when `false`/absent it leaves
73
+ * `think` unset so the daemon uses its default. Explicit
74
+ * `true`/`false` always wins over inference.
75
+ */
76
+ reasoning?: boolean;
67
77
  };
68
78
  /**
69
79
  * Per-embedder configuration for `OllamaSDK.embedder()`. `name` is the
@@ -1 +1 @@
1
- {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/config.type.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;;;;;;;;AAOuC;AAYvC;;;;;;;;AAekB;KAlCN,eAAA,GAAkB,OAAA,CAAQ,MAAA;EACpC,QAAA;;;AA6C+C;;;EAvC/C,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;;KAYf,iBAAA,GAAoB,WAAW;;;;;;;EAOzC,MAAA;;;;;;;;EAQA,gBAAA;AAAA;;;;;;;;;;KAYU,oBAAA,GAAuB,cAAc"}
1
+ {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/config.type.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;;;;;;;;AAOuC;AAYvC;;;;;;;;;KAnBY,eAAA,GAAkB,OAAA,CAAQ,MAAA;EACpC,QAAA;EAuD8B;;;AAAiB;;EAjD/C,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;;KAYf,iBAAA,GAAoB,WAAW;;;;;;;EAOzC,MAAA;;;;;;;;EAQA,gBAAA;;;;;;;;;;EAUA,SAAA;AAAA;;;;;;;;;;KAYU,oBAAA,GAAuB,cAAc"}
@@ -0,0 +1,47 @@
1
+ //#region ../@warlock.js/ai-ollama/src/known-reasoning-models.ts
2
+ /**
3
+ * Substrings identifying Ollama model tags whose family emits a
4
+ * reasoning / "thinking" channel before the visible answer.
5
+ *
6
+ * Ollama exposes thinking via the request-side `think` flag and the
7
+ * response-side `message.thinking` string. Only models trained for it
8
+ * honor `think`; sending it to a non-reasoning model is a no-op at best.
9
+ * Tags are family-named with optional size/quant suffixes
10
+ * (`deepseek-r1:7b`, `qwq:32b-preview`), so a substring match tolerates
11
+ * the suffixes. Covers the common reasoning families on the Ollama
12
+ * registry; plain instruct models (`llama3.1`, `mistral`, `phi3`) are
13
+ * excluded. Override per-model via
14
+ * `ollama.model({ name, reasoning: true | false })`.
15
+ */
16
+ const REASONING_CAPABLE_SUBSTRINGS = [
17
+ "deepseek-r1",
18
+ "qwq",
19
+ "qwen3",
20
+ "magistral",
21
+ "phi4-reasoning",
22
+ "phi4-mini-reasoning",
23
+ "cogito",
24
+ "smallthinker",
25
+ "exaone-deep",
26
+ "gpt-oss"
27
+ ];
28
+ /**
29
+ * Infer whether an Ollama model tag supports a reasoning / thinking
30
+ * channel based on the known thinking-family substrings. Unknown tags
31
+ * default to `false` so the adapter never sends the `think` flag to a
32
+ * model that cannot honor it (a no-op for plain instruct models).
33
+ *
34
+ * @example
35
+ * inferReasoningCapability("deepseek-r1:7b"); // → true
36
+ * inferReasoningCapability("qwq:32b"); // → true
37
+ * inferReasoningCapability("llama3.1"); // → false
38
+ * inferReasoningCapability("nomic-embed-text"); // → false
39
+ */
40
+ function inferReasoningCapability(modelName) {
41
+ const normalized = modelName.toLowerCase();
42
+ return REASONING_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));
43
+ }
44
+
45
+ //#endregion
46
+ export { inferReasoningCapability };
47
+ //# sourceMappingURL=known-reasoning-models.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-reasoning-models.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-ollama/src/known-reasoning-models.ts"],"sourcesContent":["/**\n * Substrings identifying Ollama model tags whose family emits a\n * reasoning / \"thinking\" channel before the visible answer.\n *\n * Ollama exposes thinking via the request-side `think` flag and the\n * response-side `message.thinking` string. Only models trained for it\n * honor `think`; sending it to a non-reasoning model is a no-op at best.\n * Tags are family-named with optional size/quant suffixes\n * (`deepseek-r1:7b`, `qwq:32b-preview`), so a substring match tolerates\n * the suffixes. Covers the common reasoning families on the Ollama\n * registry; plain instruct models (`llama3.1`, `mistral`, `phi3`) are\n * excluded. Override per-model via\n * `ollama.model({ name, reasoning: true | false })`.\n */\nconst REASONING_CAPABLE_SUBSTRINGS = [\n \"deepseek-r1\",\n \"qwq\",\n \"qwen3\",\n \"magistral\",\n \"phi4-reasoning\",\n \"phi4-mini-reasoning\",\n \"cogito\",\n \"smallthinker\",\n \"exaone-deep\",\n \"gpt-oss\",\n];\n\n/**\n * Infer whether an Ollama model tag supports a reasoning / thinking\n * channel based on the known thinking-family substrings. Unknown tags\n * default to `false` so the adapter never sends the `think` flag to a\n * model that cannot honor it (a no-op for plain instruct models).\n *\n * @example\n * inferReasoningCapability(\"deepseek-r1:7b\"); // → true\n * inferReasoningCapability(\"qwq:32b\"); // → true\n * inferReasoningCapability(\"llama3.1\"); // → false\n * inferReasoningCapability(\"nomic-embed-text\"); // → false\n */\nexport function inferReasoningCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return REASONING_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,+BAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;AAcA,SAAgB,yBAAyB,WAA4B;CACnE,MAAM,aAAa,UAAU,YAAY;CAEzC,OAAO,6BAA6B,MAAM,aAAa,WAAW,SAAS,QAAQ,CAAC;AACtF"}
package/esm/model.mjs CHANGED
@@ -3,6 +3,7 @@ import { toOllamaMessages } from "./utils/to-ollama-messages.mjs";
3
3
  import { toOllamaTools } from "./utils/to-ollama-tools.mjs";
4
4
  import { wrapOllamaError } from "./utils/wrap-ollama-error.mjs";
5
5
  import "./utils/index.mjs";
6
+ import { inferReasoningCapability } from "./known-reasoning-models.mjs";
6
7
  import { inferVisionCapability } from "./known-vision-models.mjs";
7
8
  import { log } from "@warlock.js/logger";
8
9
 
@@ -54,7 +55,11 @@ var OllamaModel = class {
54
55
  this.pricing = config.pricing;
55
56
  this.capabilities = {
56
57
  structuredOutput: config.structuredOutput ?? true,
57
- vision: config.vision ?? inferVisionCapability(config.name)
58
+ vision: config.vision ?? inferVisionCapability(config.name),
59
+ reasoning: config.reasoning ?? inferReasoningCapability(config.name),
60
+ promptCaching: false,
61
+ audio: false,
62
+ pdf: false
58
63
  };
59
64
  }
60
65
  /**
@@ -180,10 +185,31 @@ var OllamaModel = class {
180
185
  messages: toOllamaMessages(messages),
181
186
  ...Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {},
182
187
  ...this.buildTools(options?.tools),
183
- ...this.buildFormat(options?.responseSchema)
188
+ ...this.buildFormat(options?.responseSchema),
189
+ ...this.buildThink(options?.reasoning)
184
190
  };
185
191
  }
186
192
  /**
193
+ * Translate the neutral `reasoning` hint into Ollama's `think`
194
+ * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'
195
+ * | 'high'`, so the neutral `ReasoningEffort` literals pass straight
196
+ * through; an effort-less `reasoning` (only `maxTokens`, or an empty
197
+ * object) becomes `think: true` to switch the channel on.
198
+ *
199
+ * No-ops unless the model is reasoning-capable, so the `think` flag is
200
+ * never sent to a plain instruct model that cannot honor it.
201
+ *
202
+ * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama
203
+ * equivalent — the daemon does not accept a thinking-token cap — so it
204
+ * is honored only as the on/off signal above and otherwise ignored.
205
+ * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no
206
+ * provider prompt cache, so there is no cache breakpoint to place.
207
+ */
208
+ buildThink(reasoning) {
209
+ if (!reasoning || !this.capabilities.reasoning) return {};
210
+ return { think: reasoning.effort ?? true };
211
+ }
212
+ /**
187
213
  * Spread-friendly tools fragment. Empty object when no tools were
188
214
  * supplied so the caller can unconditionally spread it.
189
215
  */
@@ -220,8 +246,17 @@ var OllamaModel = class {
220
246
  }
221
247
  /**
222
248
  * Normalize Ollama's eval counts into the neutral `Usage` shape.
223
- * Ollama runs locally with no prompt cache, so there is no
224
- * `cachedTokens`; `total` is computed from input + output.
249
+ *
250
+ * Cost-truth: Ollama reports only `prompt_eval_count` (input) and
251
+ * `eval_count` (output). It has **no** provider prompt cache, so
252
+ * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined
253
+ * (honest absence, not a false zero). Reasoning models emit their
254
+ * thinking as the `message.thinking` *string* but the wire format
255
+ * carries **no separate reasoning-token count** — the thinking tokens
256
+ * are already folded into `eval_count`. We therefore do not fabricate
257
+ * a `Usage.reasoningTokens` from the text length; it is left undefined
258
+ * unless/until the daemon exposes a real count. `total` is input +
259
+ * output.
225
260
  */
226
261
  extractUsage(response) {
227
262
  const input = response.prompt_eval_count ?? 0;
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../@warlock.js/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 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 { inferVisionCapability } from \"./known-vision-models\";\nimport { mapDoneReason, toOllamaMessages, toOllamaTools, wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\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 };\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 };\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 * Ollama runs locally with no prompt cache, so there is no\n * `cachedTokens`; `total` is computed from input + 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":["../../../../../../@warlock.js/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 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/**\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 the neutral `ReasoningEffort` literals pass straight\n * through; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\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 return { think: 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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"}
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. Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`; "use ollama with warlock", "run llama3 locally", "self-hosted llama"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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: `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 — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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`
@@ -44,8 +44,28 @@ ollama.model({ name: "llama3.2-vision", maxTokens: 1024 })
44
44
  | --- | --- |
45
45
  | `structuredOutput` | `true` (via Ollama's native `format` JSON-schema field) |
46
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. |
47
50
 
48
- Explicit config always wins.
51
+ Explicit config always wins (`vision`, `structuredOutput`, `reasoning` overrides on `ollama.model({ … })`).
52
+
53
+ ## Reasoning / thinking
54
+
55
+ For thinking-capable families the adapter maps `ModelCallOptions.reasoning` onto Ollama's `think` request flag (`boolean | 'low' | 'medium' | 'high'`):
56
+
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 })
61
+ ```
62
+
63
+ - `reasoning.effort` (`low` / `medium` / `high`) passes straight through to `think`.
64
+ - `reasoning.maxTokens` (the thinking-budget hint) has **no Ollama equivalent** — the daemon takes no thinking-token cap, so it only acts as the on/off signal (`think: true`) and is otherwise ignored.
65
+ - The `think` flag is sent **only** to a `reasoning`-capable model; on a plain instruct model `reasoning` options are a no-op (no `think` is sent).
66
+ - Set `ollama.model({ name, reasoning: false })` to force it off, or `reasoning: true` to force it on for a tag the substring list doesn't recognize.
67
+
68
+ `ModelCallOptions.cacheControl` is a **no-op** — Ollama has no prompt cache, so there is no breakpoint to place.
49
69
 
50
70
  ## System prompt & roles
51
71
 
@@ -101,12 +121,18 @@ Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. The `ollama` client
101
121
  - 4xx with context phrasing → `ContextLengthExceededError`, else `InvalidRequestError`
102
122
  - 5xx → `ProviderError`
103
123
 
104
- ## Token counting
124
+ ## Token counting & usage (cost-truth)
105
125
 
106
126
  ```ts
107
127
  await ollama.count("some text") // approximate heuristic, offline
108
128
  ```
109
129
 
130
+ 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
+
132
+ - `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).
135
+
110
136
  ## When NOT to use this skill
111
137
 
112
138
  - Direct `ollama` client calls without going through `@warlock.js/ai` agents.
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. Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`; "use ollama with warlock", "run llama3 locally", "self-hosted llama"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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: `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 — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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,12 +14,12 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "ollama": "^0.6.3",
17
- "@warlock.js/logger": "4.2.11"
17
+ "@warlock.js/logger": "4.3.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@warlock.js/ai": "4.2.11"
20
+ "@warlock.js/ai": "4.3.0"
21
21
  },
22
- "version": "4.2.11",
22
+ "version": "4.3.0",
23
23
  "main": "./cjs/index.cjs",
24
24
  "module": "./esm/index.mjs",
25
25
  "types": "./esm/index.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-ollama
3
- 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. Triggers: `OllamaSDK`, `ollama.model`, `ollama.embedder`, `embedder.embedMany`, `ollama.count`, `host`, `headers`; "use ollama with warlock", "run llama3 locally", "self-hosted llama"; typical import `import { OllamaSDK } from "@warlock.js/ai-ollama"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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`.'
3
+ 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 — `@warlock.js/ai/run-ai-agent/SKILL.md`; provider choice — `@warlock.js/ai/pick-ai-provider/SKILL.md`; embeddings core — `@warlock.js/ai/embed-text/SKILL.md`; 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`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-ollama`
@@ -36,8 +36,28 @@ ollama.model({ name: "llama3.2-vision", maxTokens: 1024 })
36
36
  | --- | --- |
37
37
  | `structuredOutput` | `true` (via Ollama's native `format` JSON-schema field) |
38
38
  | `vision` | Inferred from model tag substring. `true` for `llava`, `bakllava`, `*-vision`, `moondream`, `minicpm-v`, `qwen2-vl`, `qwen2.5-vl`, `llama4`, `gemma3`; `false` otherwise. |
39
+ | `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. |
40
+ | `promptCaching` | `false` — Ollama has no provider-side prompt cache. |
41
+ | `audio` / `pdf` | `false` — the Ollama chat API takes no audio / PDF content parts. |
39
42
 
40
- Explicit config always wins.
43
+ Explicit config always wins (`vision`, `structuredOutput`, `reasoning` overrides on `ollama.model({ … })`).
44
+
45
+ ## Reasoning / thinking
46
+
47
+ For thinking-capable families the adapter maps `ModelCallOptions.reasoning` onto Ollama's `think` request flag (`boolean | 'low' | 'medium' | 'high'`):
48
+
49
+ ```ts
50
+ ollama.model({ name: "deepseek-r1:7b" })
51
+ await model.complete(messages, { reasoning: { effort: "high" } }); // → chat({ think: "high" })
52
+ await model.complete(messages, { reasoning: { maxTokens: 2048 } }); // → chat({ think: true })
53
+ ```
54
+
55
+ - `reasoning.effort` (`low` / `medium` / `high`) passes straight through to `think`.
56
+ - `reasoning.maxTokens` (the thinking-budget hint) has **no Ollama equivalent** — the daemon takes no thinking-token cap, so it only acts as the on/off signal (`think: true`) and is otherwise ignored.
57
+ - The `think` flag is sent **only** to a `reasoning`-capable model; on a plain instruct model `reasoning` options are a no-op (no `think` is sent).
58
+ - Set `ollama.model({ name, reasoning: false })` to force it off, or `reasoning: true` to force it on for a tag the substring list doesn't recognize.
59
+
60
+ `ModelCallOptions.cacheControl` is a **no-op** — Ollama has no prompt cache, so there is no breakpoint to place.
41
61
 
42
62
  ## System prompt & roles
43
63
 
@@ -93,12 +113,18 @@ Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. The `ollama` client
93
113
  - 4xx with context phrasing → `ContextLengthExceededError`, else `InvalidRequestError`
94
114
  - 5xx → `ProviderError`
95
115
 
96
- ## Token counting
116
+ ## Token counting & usage (cost-truth)
97
117
 
98
118
  ```ts
99
119
  await ollama.count("some text") // approximate heuristic, offline
100
120
  ```
101
121
 
122
+ 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:
123
+
124
+ - `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`).
125
+ - `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.
126
+ - `cost` — populated only when a `ModelPricing` is configured (local Ollama is free, so usually undefined).
127
+
102
128
  ## When NOT to use this skill
103
129
 
104
130
  - Direct `ollama` client calls without going through `@warlock.js/ai` agents.