@warlock.js/ai-ollama 5.29.0 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"known-reasoning-models.mjs","names":[],"sources":["../../../../../../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"}
1
+ {"version":3,"file":"known-reasoning-models.mjs","names":[],"sources":["../../../../../../../packages/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"}
@@ -1,4 +1,4 @@
1
- //#region ../ai-ollama/src/known-vision-models.ts
1
+ //#region ../../packages/ai-ollama/src/known-vision-models.ts
2
2
  /**
3
3
  * Substrings identifying Ollama model tags whose family accepts image
4
4
  * input (vision).
@@ -1 +1 @@
1
- {"version":3,"file":"known-vision-models.mjs","names":[],"sources":["../../../../../../ai-ollama/src/known-vision-models.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;AAWA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;AAeA,SAAgB,sBAAsB,WAA4B;CAChE,MAAM,aAAa,UAAU,YAAY;CAEzC,OAAO,0BAA0B,MAAM,aAAa,WAAW,SAAS,QAAQ,CAAC;AACnF"}
1
+ {"version":3,"file":"known-vision-models.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/known-vision-models.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;AAWA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;AAeA,SAAgB,sBAAsB,WAA4B;CAChE,MAAM,aAAa,UAAU,YAAY;CAEzC,OAAO,0BAA0B,MAAM,aAAa,WAAW,SAAS,QAAQ,CAAC;AACnF"}
package/esm/model.mjs CHANGED
@@ -1,14 +1,19 @@
1
+ import { withCallSignal } from "./utils/call-signal.mjs";
1
2
  import { mapDoneReason } from "./utils/map-done-reason.mjs";
2
3
  import { toOllamaMessages } from "./utils/to-ollama-messages.mjs";
3
4
  import { toOllamaTools } from "./utils/to-ollama-tools.mjs";
4
5
  import { wrapOllamaError } from "./utils/wrap-ollama-error.mjs";
5
6
  import "./utils/index.mjs";
7
+ import { mergeProviderBody } from "./provider-options.mjs";
6
8
  import { inferReasoningCapability } from "./known-reasoning-models.mjs";
7
9
  import { inferVisionCapability } from "./known-vision-models.mjs";
10
+ import { attachCost, resolveStreamChunks, selectProviderTools } from "@warlock.js/ai";
8
11
  import { log } from "@warlock.js/logger";
9
12
 
10
- //#region ../ai-ollama/src/model.ts
13
+ //#region ../../packages/ai-ollama/src/model.ts
11
14
  const LOG_MODULE = "ai.ollama";
15
+ /** `provider` value of `ai.providerTool()` entries this adapter sends verbatim. */
16
+ const OLLAMA_WIRE_FORMAT = "ollama";
12
17
  /** Ollama supports only low, medium, and high named thinking levels. */
13
18
  const EFFORT_TO_THINK_LEVEL = {
14
19
  minimal: "low",
@@ -75,7 +80,8 @@ var OllamaModel = class {
75
80
  * Single-shot completion. Sends the full message list to
76
81
  * `client.chat`, waits for the terminal response, and reshapes it
77
82
  * into a vendor-neutral `ModelResponse`. Per-call `options` override
78
- * the instance defaults for this call only.
83
+ * the instance defaults for this call only. Aborting `options.signal`
84
+ * cancels the HTTP request through the SDK client's signal-aware `fetch`.
79
85
  */
80
86
  async complete(messages, options) {
81
87
  this.logger.debug(LOG_MODULE, "request", "Starting chat call", {
@@ -86,16 +92,17 @@ var OllamaModel = class {
86
92
  });
87
93
  let response;
88
94
  try {
89
- response = await this.client.chat({
95
+ options?.signal?.throwIfAborted();
96
+ response = await withCallSignal(options?.signal, () => this.client.chat({
90
97
  ...this.buildRequest(messages, options),
91
98
  stream: false
92
- });
99
+ }));
93
100
  } catch (thrown) {
94
101
  throw this.logAndWrap(thrown);
95
102
  }
96
103
  const toolCalls = this.extractToolCalls(response.message);
97
104
  const finishReason = toolCalls ? "tool_calls" : mapDoneReason(response.done_reason);
98
- const usage = this.extractUsage(response);
105
+ const usage = attachCost(this.extractUsage(response), this.pricing);
99
106
  this.logger.debug(LOG_MODULE, "response", "chat call succeeded", {
100
107
  finishReason,
101
108
  usage
@@ -112,7 +119,12 @@ var OllamaModel = class {
112
119
  * `ModelStreamChunk`s — `delta` for content, `tool-call` per
113
120
  * function call (Ollama streams a fully-formed call, not partial
114
121
  * JSON), and a terminal `done` with the final finish reason + usage.
115
- * Honors `options.signal` by aborting the underlying stream.
122
+ * Honors `options.signal`: aborting cancels the HTTP request (through
123
+ * the SDK client's signal-aware `fetch`) and the stream iterator.
124
+ *
125
+ * With `options.streamChunks` it also yields `reasoning` for each
126
+ * `message.thinking` piece. Usage arrives only on the final chunk, so no
127
+ * `usage` chunk; calls arrive whole, so no `tool-args`.
116
128
  */
117
129
  async *stream(messages, options) {
118
130
  this.logger.debug(LOG_MODULE, "request", "Starting streaming chat call", {
@@ -123,10 +135,11 @@ var OllamaModel = class {
123
135
  });
124
136
  let stream;
125
137
  try {
126
- stream = await this.client.chat({
138
+ options?.signal?.throwIfAborted();
139
+ stream = await withCallSignal(options?.signal, () => this.client.chat({
127
140
  ...this.buildRequest(messages, options),
128
141
  stream: true
129
- });
142
+ }));
130
143
  } catch (thrown) {
131
144
  throw this.logAndWrap(thrown);
132
145
  }
@@ -139,8 +152,14 @@ var OllamaModel = class {
139
152
  output: 0,
140
153
  total: 0
141
154
  };
155
+ const want = resolveStreamChunks(options);
142
156
  try {
143
157
  for await (const chunk of stream) {
158
+ const thinking = chunk.message?.thinking;
159
+ if (want.reasoning && thinking) yield {
160
+ type: "reasoning",
161
+ delta: thinking
162
+ };
144
163
  const content = chunk.message?.content;
145
164
  if (content) yield {
146
165
  type: "delta",
@@ -166,6 +185,7 @@ var OllamaModel = class {
166
185
  throw this.logAndWrap(thrown);
167
186
  }
168
187
  const finishReason = sawToolCall ? "tool_calls" : mapDoneReason(rawDoneReason);
188
+ attachCost(usage, this.pricing);
169
189
  this.logger.debug(LOG_MODULE, "response", "streaming chat call succeeded", {
170
190
  finishReason,
171
191
  usage
@@ -189,14 +209,14 @@ var OllamaModel = class {
189
209
  ...temperature !== void 0 ? { temperature } : {},
190
210
  ...maxTokens !== void 0 ? { num_predict: maxTokens } : {}
191
211
  };
192
- return {
212
+ return mergeProviderBody({
193
213
  model: this.name,
194
214
  messages: toOllamaMessages(messages),
195
215
  ...Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {},
196
- ...this.buildTools(options?.tools),
216
+ ...this.buildTools(options),
197
217
  ...this.buildFormat(options?.responseSchema),
198
218
  ...this.buildThink(options?.reasoning)
199
- };
219
+ }, options);
200
220
  }
201
221
  /**
202
222
  * Translate the neutral `reasoning` hint into Ollama's `think`
@@ -226,9 +246,15 @@ var OllamaModel = class {
226
246
  * Spread-friendly tools fragment. Empty object when no tools were
227
247
  * supplied so the caller can unconditionally spread it.
228
248
  */
229
- buildTools(tools) {
230
- const mapped = toOllamaTools(tools);
231
- return mapped ? { tools: mapped } : {};
249
+ buildTools(options) {
250
+ const { tools, definitions } = selectProviderTools(options, {
251
+ accepts: [OLLAMA_WIRE_FORMAT, this.provider],
252
+ module: LOG_MODULE,
253
+ model: this.name,
254
+ logger: this.logger
255
+ });
256
+ const mapped = [...toOllamaTools(tools) ?? [], ...definitions];
257
+ return mapped.length > 0 ? { tools: mapped } : {};
232
258
  }
233
259
  /**
234
260
  * Translate the neutral `responseSchema` into Ollama's native
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../ai-ollama/src/model.ts"],"sourcesContent":["import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n AbortableAsyncIterator,\n ChatRequest,\n ChatResponse,\n Ollama,\n Options,\n} from \"ollama\";\nimport type { OllamaModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapDoneReason, toOllamaMessages, toOllamaTools, wrapOllamaError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/** Ollama supports only low, medium, and high named thinking levels. */\nconst EFFORT_TO_THINK_LEVEL: Record<\n Exclude<ReasoningEffort, \"none\">,\n \"low\" | \"medium\" | \"high\"\n> = {\n minimal: \"low\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"high\",\n max: \"high\",\n};\n\n/**\n * Ollama-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and a local (or self-hosted) Ollama\n * server via the official `ollama` client.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client + frozen `ModelConfig` (model\n * tag, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Ollama's chat shapes (system stays a real role, `tool_calls` /\n * `tool_name`, base64 `images`) and Ollama's response (content, tool\n * calls, done reason, eval-count usage) back into neutral shapes.\n * - Does NOT own: tool dispatch, looping, history, retries — agent\n * concerns. The model is a per-call protocol adapter.\n *\n * **Tool-call ids.** Ollama has no tool-call id concept — a `tool_call`\n * is `{ function: { name, arguments } }`. The adapter synthesizes the\n * neutral `id` from the tool name so the agent's tool-result round-trip\n * (which keys on `toolCallId`) maps back to Ollama's name-based\n * matching. Parallel calls to the *same* tool in one turn therefore\n * share an id — a documented v1 limitation inherent to Ollama's wire\n * format, not this adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\").\n *\n * @example\n * import { Ollama } from \"ollama\";\n * const client = new Ollama({ host: \"http://127.0.0.1:11434\" });\n * const model = new OllamaModel(client, { name: \"llama3.1\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OllamaModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: Ollama;\n private readonly config: OllamaModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: Ollama, config: OllamaModelConfig, provider: string = \"ollama\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n // Thinking-capable families (deepseek-r1, qwq, qwen3, …) honor the\n // `think` request flag; plain instruct models do not. Explicit\n // config wins over the family-substring inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Ollama has no provider-side prompt cache and the chat API takes\n // no audio / PDF content parts — report these truthfully as false\n // so the agent rejects unsupported attachments / cache hints\n // upfront instead of silently dropping them at the wire.\n promptCaching: false,\n audio: false,\n pdf: false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `client.chat`, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override\n * the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: ChatResponse;\n\n try {\n response = await this.client.chat({ ...this.buildRequest(messages, options), stream: false });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response.message);\n const finishReason = toolCalls ? \"tool_calls\" : mapDoneReason(response.done_reason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"chat call succeeded\", { finishReason, usage });\n\n return {\n content: response.message?.content ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral\n * `ModelStreamChunk`s — `delta` for content, `tool-call` per\n * function call (Ollama streams a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n * Honors `options.signal` by aborting the underlying stream.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: AbortableAsyncIterator<ChatResponse>;\n\n try {\n stream = await this.client.chat({ ...this.buildRequest(messages, options), stream: true });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n stream.abort();\n } else {\n options.signal.addEventListener(\"abort\", () => stream.abort(), { once: true });\n }\n }\n\n let rawDoneReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of stream) {\n const content = chunk.message?.content;\n\n if (content) {\n yield { type: \"delta\", content };\n }\n\n for (const call of chunk.message?.tool_calls ?? []) {\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n };\n }\n\n if (chunk.done_reason) {\n rawDoneReason = chunk.done_reason;\n }\n\n if (chunk.done) {\n usage.input = chunk.prompt_eval_count ?? usage.input;\n usage.output = chunk.eval_count ?? usage.output;\n usage.total = usage.input + usage.output;\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapDoneReason(rawDoneReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"streaming chat call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the Ollama chat request shared by `complete()` and\n * `stream()` (each adds its own `stream` literal so the client's\n * overload resolves). Maps inference params into Ollama `options`\n * and conditionally attaches tools + native structured output.\n */\n private buildRequest(\n messages: Message[],\n options: ModelCallOptions | undefined,\n ): Omit<ChatRequest, \"stream\"> {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n\n const ollamaOptions: Partial<Options> = {\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxTokens !== undefined ? { num_predict: maxTokens } : {}),\n };\n\n return {\n model: this.name,\n messages: toOllamaMessages(messages),\n ...(Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildFormat(options?.responseSchema),\n ...this.buildThink(options?.reasoning),\n };\n }\n\n /**\n * Translate the neutral `reasoning` hint into Ollama's `think`\n * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'\n * | 'high'`, so neutral effort levels are clamped to the nearest named\n * level; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\n * The neutral `effort: \"none\"` (\"run without reasoning\") maps to\n * `think: false` — Ollama has no `\"none\"` level, and this is its\n * native reasoning-off switch.\n *\n * No-ops unless the model is reasoning-capable, so the `think` flag is\n * never sent to a plain instruct model that cannot honor it.\n *\n * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama\n * equivalent — the daemon does not accept a thinking-token cap — so it\n * is honored only as the on/off signal above and otherwise ignored.\n * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no\n * provider prompt cache, so there is no cache breakpoint to place.\n */\n private buildThink(\n reasoning: ModelCallOptions[\"reasoning\"],\n ): Pick<ChatRequest, \"think\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit reasoning-off → switch the channel off.\n // Ollama's `think` has no `\"none\"` level, so map it to `false`.\n if (reasoning.effort === \"none\") {\n return { think: false };\n }\n\n return { think: reasoning.effort ? EFFORT_TO_THINK_LEVEL[reasoning.effort] : true };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<ChatRequest, \"tools\"> {\n const mapped = toOllamaTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Ollama's native\n * structured output (`format` accepts a JSON Schema object).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildFormat(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<ChatRequest, \"format\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return { format: responseSchema };\n }\n\n /**\n * Reshape Ollama's `message.tool_calls` into the neutral\n * `ModelToolCallRequest[]`. Ollama has no tool-call id, so the\n * neutral `id` is synthesized from the tool name (see the class\n * doc). Returns `undefined` when no tools were requested.\n */\n private extractToolCalls(\n message: ChatResponse[\"message\"] | undefined,\n ): ModelToolCallRequest[] | undefined {\n const calls = message?.tool_calls;\n\n if (!calls || calls.length === 0) {\n return undefined;\n }\n\n return calls.map((call) => ({\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n }));\n }\n\n /**\n * Normalize Ollama's eval counts into the neutral `Usage` shape.\n *\n * Cost-truth: Ollama reports only `prompt_eval_count` (input) and\n * `eval_count` (output). It has **no** provider prompt cache, so\n * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined\n * (honest absence, not a false zero). Reasoning models emit their\n * thinking as the `message.thinking` *string* but the wire format\n * carries **no separate reasoning-token count** — the thinking tokens\n * are already folded into `eval_count`. We therefore do not fabricate\n * a `Usage.reasoningTokens` from the text length; it is left undefined\n * unless/until the daemon exposes a real count. `total` is input +\n * output.\n */\n private extractUsage(response: ChatResponse): Usage {\n const input = response.prompt_eval_count ?? 0;\n const output = response.eval_count ?? 0;\n\n return { input, output, total: input + output };\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/model.ts"],"sourcesContent":["import {\n attachCost,\n resolveStreamChunks,\n selectProviderTools,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n AbortableAsyncIterator,\n ChatRequest,\n ChatResponse,\n Ollama,\n Options,\n Tool,\n} from \"ollama\";\nimport { mergeProviderBody } from \"./provider-options\";\nimport type { OllamaModelConfig } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport {\n mapDoneReason,\n toOllamaMessages,\n toOllamaTools,\n withCallSignal,\n wrapOllamaError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.ollama\";\n\n/** `provider` value of `ai.providerTool()` entries this adapter sends verbatim. */\nconst OLLAMA_WIRE_FORMAT = \"ollama\";\n\n/** Ollama supports only low, medium, and high named thinking levels. */\nconst EFFORT_TO_THINK_LEVEL: Record<Exclude<ReasoningEffort, \"none\">, \"low\" | \"medium\" | \"high\"> = {\n minimal: \"low\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"high\",\n max: \"high\",\n};\n\n/**\n * Ollama-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and a local (or self-hosted) Ollama\n * server via the official `ollama` client.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client + frozen `ModelConfig` (model\n * tag, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Ollama's chat shapes (system stays a real role, `tool_calls` /\n * `tool_name`, base64 `images`) and Ollama's response (content, tool\n * calls, done reason, eval-count usage) back into neutral shapes.\n * - Does NOT own: tool dispatch, looping, history, retries — agent\n * concerns. The model is a per-call protocol adapter.\n *\n * **Tool-call ids.** Ollama has no tool-call id concept — a `tool_call`\n * is `{ function: { name, arguments } }`. The adapter synthesizes the\n * neutral `id` from the tool name so the agent's tool-result round-trip\n * (which keys on `toolCallId`) maps back to Ollama's name-based\n * matching. Parallel calls to the *same* tool in one turn therefore\n * share an id — a documented v1 limitation inherent to Ollama's wire\n * format, not this adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\").\n *\n * @example\n * import { Ollama } from \"ollama\";\n * const client = new Ollama({ host: \"http://127.0.0.1:11434\" });\n * const model = new OllamaModel(client, { name: \"llama3.1\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OllamaModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: Ollama;\n private readonly config: OllamaModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: Ollama, config: OllamaModelConfig, provider: string = \"ollama\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: config.vision ?? inferVisionCapability(config.name),\n // Thinking-capable families (deepseek-r1, qwq, qwen3, …) honor the\n // `think` request flag; plain instruct models do not. Explicit\n // config wins over the family-substring inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // Ollama has no provider-side prompt cache and the chat API takes\n // no audio / PDF content parts — report these truthfully as false\n // so the agent rejects unsupported attachments / cache hints\n // upfront instead of silently dropping them at the wire.\n promptCaching: false,\n audio: false,\n pdf: false,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `client.chat`, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override\n * the instance defaults for this call only. Aborting `options.signal`\n * cancels the HTTP request through the SDK client's signal-aware `fetch`.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: ChatResponse;\n\n try {\n options?.signal?.throwIfAborted();\n response = await withCallSignal(options?.signal, () =>\n this.client.chat({ ...this.buildRequest(messages, options), stream: false }),\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response.message);\n const finishReason = toolCalls ? \"tool_calls\" : mapDoneReason(response.done_reason);\n const usage = attachCost(this.extractUsage(response), this.pricing);\n\n this.logger.debug(LOG_MODULE, \"response\", \"chat call succeeded\", { finishReason, usage });\n\n return {\n content: response.message?.content ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral\n * `ModelStreamChunk`s — `delta` for content, `tool-call` per\n * function call (Ollama streams a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n * Honors `options.signal`: aborting cancels the HTTP request (through\n * the SDK client's signal-aware `fetch`) and the stream iterator.\n *\n * With `options.streamChunks` it also yields `reasoning` for each\n * `message.thinking` piece. Usage arrives only on the final chunk, so no\n * `usage` chunk; calls arrive whole, so no `tool-args`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming chat call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: AbortableAsyncIterator<ChatResponse>;\n\n try {\n options?.signal?.throwIfAborted();\n stream = await withCallSignal(options?.signal, () =>\n this.client.chat({ ...this.buildRequest(messages, options), stream: true }),\n );\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n stream.abort();\n } else {\n options.signal.addEventListener(\"abort\", () => stream.abort(), { once: true });\n }\n }\n\n let rawDoneReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const want = resolveStreamChunks(options);\n\n try {\n for await (const chunk of stream) {\n // `message.thinking` streams when the request sets `think`.\n const thinking = chunk.message?.thinking;\n\n if (want.reasoning && thinking) {\n yield { type: \"reasoning\", delta: thinking };\n }\n\n const content = chunk.message?.content;\n\n if (content) {\n yield { type: \"delta\", content };\n }\n\n for (const call of chunk.message?.tool_calls ?? []) {\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n };\n }\n\n if (chunk.done_reason) {\n rawDoneReason = chunk.done_reason;\n }\n\n if (chunk.done) {\n usage.input = chunk.prompt_eval_count ?? usage.input;\n usage.output = chunk.eval_count ?? usage.output;\n usage.total = usage.input + usage.output;\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapDoneReason(rawDoneReason);\n\n attachCost(usage, this.pricing);\n\n this.logger.debug(LOG_MODULE, \"response\", \"streaming chat call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the Ollama chat request shared by `complete()` and\n * `stream()` (each adds its own `stream` literal so the client's\n * overload resolves). Maps inference params into Ollama `options`\n * and conditionally attaches tools + native structured output.\n */\n private buildRequest(\n messages: Message[],\n options: ModelCallOptions | undefined,\n ): Omit<ChatRequest, \"stream\"> {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxTokens = options?.maxTokens ?? this.config.maxTokens;\n\n const ollamaOptions: Partial<Options> = {\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxTokens !== undefined ? { num_predict: maxTokens } : {}),\n };\n\n return mergeProviderBody<Omit<ChatRequest, \"stream\">>(\n {\n model: this.name,\n messages: toOllamaMessages(messages),\n ...(Object.keys(ollamaOptions).length > 0 ? { options: ollamaOptions } : {}),\n ...this.buildTools(options),\n ...this.buildFormat(options?.responseSchema),\n ...this.buildThink(options?.reasoning),\n },\n options,\n );\n }\n\n /**\n * Translate the neutral `reasoning` hint into Ollama's `think`\n * request flag. Ollama's `think` accepts `boolean | 'low' | 'medium'\n * | 'high'`, so neutral effort levels are clamped to the nearest named\n * level; an effort-less `reasoning` (only `maxTokens`, or an empty\n * object) becomes `think: true` to switch the channel on.\n * The neutral `effort: \"none\"` (\"run without reasoning\") maps to\n * `think: false` — Ollama has no `\"none\"` level, and this is its\n * native reasoning-off switch.\n *\n * No-ops unless the model is reasoning-capable, so the `think` flag is\n * never sent to a plain instruct model that cannot honor it.\n *\n * `reasoning.maxTokens` (the thinking-budget hint) has no Ollama\n * equivalent — the daemon does not accept a thinking-token cap — so it\n * is honored only as the on/off signal above and otherwise ignored.\n * `ModelCallOptions.cacheControl` is likewise a no-op: Ollama has no\n * provider prompt cache, so there is no cache breakpoint to place.\n */\n private buildThink(reasoning: ModelCallOptions[\"reasoning\"]): Pick<ChatRequest, \"think\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit reasoning-off → switch the channel off.\n // Ollama's `think` has no `\"none\"` level, so map it to `false`.\n if (reasoning.effort === \"none\") {\n return { think: false };\n }\n\n return { think: reasoning.effort ? EFFORT_TO_THINK_LEVEL[reasoning.effort] : true };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(options: ModelCallOptions | undefined): Pick<ChatRequest, \"tools\"> {\n // Ollama's chat API has no built-in tools; provider-defined entries\n // reach it only when they name this adapter, and others are skipped\n // with a warning.\n const { tools, definitions } = selectProviderTools(options, {\n accepts: [OLLAMA_WIRE_FORMAT, this.provider],\n module: LOG_MODULE,\n model: this.name,\n logger: this.logger,\n });\n const mapped = [...(toOllamaTools(tools) ?? []), ...(definitions as unknown as Tool[])];\n\n return mapped.length > 0 ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Ollama's native\n * structured output (`format` accepts a JSON Schema object).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildFormat(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<ChatRequest, \"format\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return { format: responseSchema };\n }\n\n /**\n * Reshape Ollama's `message.tool_calls` into the neutral\n * `ModelToolCallRequest[]`. Ollama has no tool-call id, so the\n * neutral `id` is synthesized from the tool name (see the class\n * doc). Returns `undefined` when no tools were requested.\n */\n private extractToolCalls(\n message: ChatResponse[\"message\"] | undefined,\n ): ModelToolCallRequest[] | undefined {\n const calls = message?.tool_calls;\n\n if (!calls || calls.length === 0) {\n return undefined;\n }\n\n return calls.map((call) => ({\n id: call.function.name,\n name: call.function.name,\n input: (call.function.arguments ?? {}) as Record<string, unknown>,\n }));\n }\n\n /**\n * Normalize Ollama's eval counts into the neutral `Usage` shape.\n *\n * Cost-truth: Ollama reports only `prompt_eval_count` (input) and\n * `eval_count` (output). It has **no** provider prompt cache, so\n * `Usage.cachedTokens` / `Usage.cacheWriteTokens` stay undefined\n * (honest absence, not a false zero). Reasoning models emit their\n * thinking as the `message.thinking` *string* but the wire format\n * carries **no separate reasoning-token count** — the thinking tokens\n * are already folded into `eval_count`. We therefore do not fabricate\n * a `Usage.reasoningTokens` from the text length; it is left undefined\n * unless/until the daemon exposes a real count. `total` is input +\n * output.\n */\n private extractUsage(response: ChatResponse): Usage {\n const input = response.prompt_eval_count ?? 0;\n const output = response.eval_count ?? 0;\n\n return { input, output, total: input + output };\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapOllamaError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ //#region ../../packages/ai-ollama/src/provider-options.d.ts
2
+ /** Adapter-only fields merged after the neutral request parameters. */
3
+ type OllamaCallOptions = {
4
+ body?: Record<string, unknown>;
5
+ };
6
+ //#endregion
7
+ export { OllamaCallOptions };
8
+ //# sourceMappingURL=provider-options.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/provider-options.ts"],"mappings":";;KAGY,iBAAA;EAAsB,IAAA,GAAO,MAAM;AAAA"}
@@ -0,0 +1,36 @@
1
+ import { InvalidRequestError } from "@warlock.js/ai";
2
+
3
+ //#region ../../packages/ai-ollama/src/provider-options.ts
4
+ const RESERVED_FIELDS = new Set([
5
+ "model",
6
+ "messages",
7
+ "tools",
8
+ "stream"
9
+ ]);
10
+ function mergeProviderBody(built, options) {
11
+ const body = (options?.providerOptions?.ollama)?.body ?? {};
12
+ const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));
13
+ if (reserved.length > 0) throw new InvalidRequestError(`providerOptions.ollama.body cannot set ${reserved.join(", ")}: use the call's messages, tools and model options.`, { context: { fields: reserved } });
14
+ const defaults = built;
15
+ const overrides = Object.fromEntries(Object.entries(body).map(([key, value]) => {
16
+ const previous = defaults[key];
17
+ return [key, isPlainObject(previous) && isPlainObject(value) ? {
18
+ ...previous,
19
+ ...value
20
+ } : value];
21
+ }));
22
+ return {
23
+ ...built,
24
+ ...overrides
25
+ };
26
+ }
27
+ /** Merge only plain objects; arrays, null and provider scalar values replace the field. */
28
+ function isPlainObject(value) {
29
+ if (value === null || typeof value !== "object") return false;
30
+ const prototype = Object.getPrototypeOf(value);
31
+ return prototype === Object.prototype || prototype === null;
32
+ }
33
+
34
+ //#endregion
35
+ export { mergeProviderBody };
36
+ //# sourceMappingURL=provider-options.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/provider-options.ts"],"sourcesContent":["import { InvalidRequestError, type ModelCallOptions } from \"@warlock.js/ai\";\n\n/** Adapter-only fields merged after the neutral request parameters. */\nexport type OllamaCallOptions = { body?: Record<string, unknown> };\n\nconst RESERVED_FIELDS = new Set([\"model\", \"messages\", \"tools\", \"stream\"]);\n\nexport function mergeProviderBody<T extends object>(\n built: T,\n options: ModelCallOptions | undefined,\n): T {\n const provider = options?.providerOptions?.ollama as OllamaCallOptions | undefined;\n const body = provider?.body ?? {};\n const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));\n if (reserved.length > 0) {\n throw new InvalidRequestError(\n `providerOptions.ollama.body cannot set ${reserved.join(\", \")}: use the call's messages, tools and model options.`,\n { context: { fields: reserved } },\n );\n }\n const defaults = built as Record<string, unknown>;\n const overrides = Object.fromEntries(\n Object.entries(body).map(([key, value]) => {\n const previous = defaults[key];\n return [\n key,\n isPlainObject(previous) && isPlainObject(value) ? { ...previous, ...value } : value,\n ];\n }),\n );\n return { ...built, ...overrides };\n}\n\n/** Merge only plain objects; arrays, null and provider scalar values replace the field. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n"],"mappings":";;;AAKA,MAAM,kBAAkB,IAAI,IAAI;CAAC;CAAS;CAAY;CAAS;AAAQ,CAAC;AAExE,SAAgB,kBACd,OACA,SACG;CAEH,MAAM,QADW,SAAS,iBAAiB,OACtB,EAAE,QAAQ,CAAC;CAChC,MAAM,WAAW,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,QAAQ,gBAAgB,IAAI,GAAG,CAAC;CAC3E,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,oBACR,0CAA0C,SAAS,KAAK,IAAI,EAAE,sDAC9D,EAAE,SAAS,EAAE,QAAQ,SAAS,EAAE,CAClC;CAEF,MAAM,WAAW;CACjB,MAAM,YAAY,OAAO,YACvB,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW;EACzC,MAAM,WAAW,SAAS;EAC1B,OAAO,CACL,KACA,cAAc,QAAQ,KAAK,cAAc,KAAK,IAAI;GAAE,GAAG;GAAU,GAAG;EAAM,IAAI,KAChF;CACF,CAAC,CACH;CACA,OAAO;EAAE,GAAG;EAAO,GAAG;CAAU;AAClC;;AAGA,SAAS,cAAc,OAAkD;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD"}
package/esm/sdk.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { OllamaEmbedderConfig, OllamaModelConfig, OllamaSDKConfig } from "./config.type.mjs";
2
2
  import { EmbedderContract, ModelContract, SDKAdapterContract } from "@warlock.js/ai";
3
3
 
4
- //#region ../ai-ollama/src/sdk.d.ts
4
+ //#region ../../packages/ai-ollama/src/sdk.d.ts
5
5
  /**
6
6
  * Ollama-backed implementation of `SDKAdapterContract`.
7
7
  *
package/esm/sdk.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../ai-ollama/src/sdk.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,GAAQ,eAAA;EAgCwB;;;;;;AAWY;;;EA1BxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
1
+ {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"mappings":";;;;;;AAoCA;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,MAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,GAAQ,eAAA;EAsCwB;;;;;;AAWY;;;EA1BxD,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;;;;;;;;EAe5B,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;AAAA"}
package/esm/sdk.mjs CHANGED
@@ -1,9 +1,11 @@
1
+ import { signalAwareFetch } from "./utils/call-signal.mjs";
2
+ import "./utils/index.mjs";
1
3
  import { OllamaEmbedder } from "./embedder.mjs";
2
4
  import { OllamaModel } from "./model.mjs";
3
5
  import { Ollama } from "ollama";
4
6
  import { approximateTokenCount } from "@warlock.js/ai";
5
7
 
6
- //#region ../ai-ollama/src/sdk.ts
8
+ //#region ../../packages/ai-ollama/src/sdk.ts
7
9
  /**
8
10
  * Ollama-backed implementation of `SDKAdapterContract`.
9
11
  *
@@ -30,7 +32,10 @@ import { approximateTokenCount } from "@warlock.js/ai";
30
32
  var OllamaSDK = class {
31
33
  constructor(config = {}) {
32
34
  const { provider, pricing, ...clientConfig } = config;
33
- this.client = new Ollama(clientConfig);
35
+ this.client = new Ollama({
36
+ ...clientConfig,
37
+ fetch: signalAwareFetch(clientConfig.fetch ?? globalThis.fetch)
38
+ });
34
39
  this.provider = provider ?? "ollama";
35
40
  this.pricing = pricing;
36
41
  }
package/esm/sdk.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../ai-ollama/src/sdk.ts"],"sourcesContent":["import { Ollama } from \"ollama\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n OllamaEmbedderConfig,\n OllamaModelConfig,\n OllamaSDKConfig,\n} from \"./config.type\";\nimport { OllamaEmbedder } from \"./embedder\";\nimport { OllamaModel } from \"./model\";\n\n/**\n * Ollama-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for local / self-hosted models\n * served by an Ollama daemon via the official `ollama` client. One\n * `OllamaSDK` holds one live `Ollama` client, shared by every\n * `ModelContract` / `EmbedderContract` it produces.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client (host, headers) and its\n * lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances\n * sharing that client.\n * - Does NOT own: anything per-call — those live in `OllamaModel` /\n * `OllamaEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const ollama = new OllamaSDK({}); // local default host\n * const model = ollama.model({ name: \"llama3.1\", temperature: 0.7 });\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n */\nexport class OllamaSDK implements SDKAdapterContract {\n private readonly client: Ollama;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OllamaSDKConfig = {}) {\n const { provider, pricing, ...clientConfig } = config;\n\n this.client = new Ollama(clientConfig);\n this.provider = provider ?? \"ollama\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OllamaModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `Ollama` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (local Ollama is free, so usually undefined).\n */\n public model(config: OllamaModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OllamaModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OllamaModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — good enough for\n * budgeting / context guards, not billing (and Ollama is free\n * anyway). The optional model id is reserved for future per-model\n * tokenizer dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OllamaEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OllamaEmbedderConfig): EmbedderContract {\n return new OllamaEmbedder(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,SAA0B,CAAC,GAAG;EAC/C,MAAM,EAAE,UAAU,SAAS,GAAG,iBAAiB;EAE/C,KAAK,SAAS,IAAI,OAAO,YAAY;EACrC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CAC9D;AACF"}
1
+ {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../../packages/ai-ollama/src/sdk.ts"],"sourcesContent":["import { Ollama } from \"ollama\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OllamaEmbedderConfig, OllamaModelConfig, OllamaSDKConfig } from \"./config.type\";\nimport { OllamaEmbedder } from \"./embedder\";\nimport { OllamaModel } from \"./model\";\nimport { signalAwareFetch } from \"./utils\";\n\n/**\n * Ollama-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for local / self-hosted models\n * served by an Ollama daemon via the official `ollama` client. One\n * `OllamaSDK` holds one live `Ollama` client, shared by every\n * `ModelContract` / `EmbedderContract` it produces.\n *\n * **Responsibility.**\n * - Owns: a long-lived `Ollama` client (host, headers) and its\n * lifetime. Factory for `OllamaModel` / `OllamaEmbedder` instances\n * sharing that client.\n * - Does NOT own: anything per-call — those live in `OllamaModel` /\n * `OllamaEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const ollama = new OllamaSDK({}); // local default host\n * const model = ollama.model({ name: \"llama3.1\", temperature: 0.7 });\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n */\nexport class OllamaSDK implements SDKAdapterContract {\n private readonly client: Ollama;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OllamaSDKConfig = {}) {\n const { provider, pricing, ...clientConfig } = config;\n\n // Route every request through a fetch that honours the current call's\n // `ModelCallOptions.signal` — the `ollama` client takes no per-request\n // signal of its own (see `utils/call-signal.ts`).\n this.client = new Ollama({\n ...clientConfig,\n fetch: signalAwareFetch(clientConfig.fetch ?? globalThis.fetch),\n });\n this.provider = provider ?? \"ollama\";\n this.pricing = pricing;\n }\n\n /**\n * Build an `OllamaModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `Ollama` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (local Ollama is free, so usually undefined).\n */\n public model(config: OllamaModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OllamaModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OllamaModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — good enough for\n * budgeting / context guards, not billing (and Ollama is free\n * anyway). The optional model id is reserved for future per-model\n * tokenizer dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OllamaEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = ollama.embedder({ name: \"nomic-embed-text\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OllamaEmbedderConfig): EmbedderContract {\n return new OllamaEmbedder(this.client, config, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,SAA0B,CAAC,GAAG;EAC/C,MAAM,EAAE,UAAU,SAAS,GAAG,iBAAiB;EAK/C,KAAK,SAAS,IAAI,OAAO;GACvB,GAAG;GACH,OAAO,iBAAiB,aAAa,SAAS,WAAW,KAAK;EAChE,CAAC;EACD,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,QAAQ,gBAAgB,KAAK,QAAQ;CACnE;;;;;;;;CASA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,QAAQ,QAAQ,KAAK,QAAQ;CAC9D;AACF"}
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ //#region ../../packages/ai-ollama/src/utils/call-signal.ts
4
+ /**
5
+ * Per-call cancellation for the `ollama` client.
6
+ *
7
+ * The `ollama` client (0.6) takes no per-request `AbortSignal`: a
8
+ * non-streaming `chat()` cannot be cancelled at all, and a streamed one only
9
+ * through the iterator it returns once the response headers have arrived.
10
+ * To honour `ModelCallOptions.signal` the SDK builds its client with
11
+ * {@link signalAwareFetch}, and the model runs each request inside
12
+ * {@link withCallSignal}; the wrapped `fetch` reads the call's signal from
13
+ * async context and hands it to the real `fetch`, so aborting cancels the
14
+ * HTTP request at any point — before the headers, or mid-body.
15
+ */
16
+ const callSignal = new AsyncLocalStorage();
17
+ /** Run `operation` with `signal` as the cancellation signal for any `fetch` it makes through {@link signalAwareFetch}. */
18
+ function withCallSignal(signal, operation) {
19
+ return callSignal.run(signal, operation);
20
+ }
21
+ /**
22
+ * Wrap a `fetch` so every request also listens to the signal of the call it
23
+ * belongs to (see {@link withCallSignal}), combined with any signal the
24
+ * client set itself.
25
+ */
26
+ function signalAwareFetch(baseFetch) {
27
+ return (input, init) => {
28
+ const signal = callSignal.getStore();
29
+ if (!signal) return baseFetch(input, init);
30
+ const own = init?.signal ?? void 0;
31
+ return baseFetch(input, {
32
+ ...init,
33
+ signal: own ? AbortSignal.any([own, signal]) : signal
34
+ });
35
+ };
36
+ }
37
+
38
+ //#endregion
39
+ export { signalAwareFetch, withCallSignal };
40
+ //# sourceMappingURL=call-signal.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-signal.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/call-signal.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Per-call cancellation for the `ollama` client.\n *\n * The `ollama` client (0.6) takes no per-request `AbortSignal`: a\n * non-streaming `chat()` cannot be cancelled at all, and a streamed one only\n * through the iterator it returns once the response headers have arrived.\n * To honour `ModelCallOptions.signal` the SDK builds its client with\n * {@link signalAwareFetch}, and the model runs each request inside\n * {@link withCallSignal}; the wrapped `fetch` reads the call's signal from\n * async context and hands it to the real `fetch`, so aborting cancels the\n * HTTP request at any point — before the headers, or mid-body.\n */\nconst callSignal = new AsyncLocalStorage<AbortSignal | undefined>();\n\n/** Run `operation` with `signal` as the cancellation signal for any `fetch` it makes through {@link signalAwareFetch}. */\nexport function withCallSignal<T>(signal: AbortSignal | undefined, operation: () => T): T {\n return callSignal.run(signal, operation);\n}\n\n/**\n * Wrap a `fetch` so every request also listens to the signal of the call it\n * belongs to (see {@link withCallSignal}), combined with any signal the\n * client set itself.\n */\nexport function signalAwareFetch(baseFetch: typeof fetch): typeof fetch {\n return (input, init) => {\n const signal = callSignal.getStore();\n\n if (!signal) {\n return baseFetch(input, init);\n }\n\n const own = init?.signal ?? undefined;\n\n return baseFetch(input, {\n ...init,\n signal: own ? AbortSignal.any([own, signal]) : signal,\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,aAAa,IAAI,kBAA2C;;AAGlE,SAAgB,eAAkB,QAAiC,WAAuB;CACxF,OAAO,WAAW,IAAI,QAAQ,SAAS;AACzC;;;;;;AAOA,SAAgB,iBAAiB,WAAuC;CACtE,QAAQ,OAAO,SAAS;EACtB,MAAM,SAAS,WAAW,SAAS;EAEnC,IAAI,CAAC,QACH,OAAO,UAAU,OAAO,IAAI;EAG9B,MAAM,MAAM,MAAM,UAAU;EAE5B,OAAO,UAAU,OAAO;GACtB,GAAG;GACH,QAAQ,MAAM,YAAY,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI;EACjD,CAAC;CACH;AACF"}
@@ -1,3 +1,4 @@
1
+ import { signalAwareFetch, withCallSignal } from "./call-signal.mjs";
1
2
  import { mapDoneReason } from "./map-done-reason.mjs";
2
3
  import { toOllamaMessages } from "./to-ollama-messages.mjs";
3
4
  import { toOllamaTools } from "./to-ollama-tools.mjs";
@@ -1,4 +1,4 @@
1
- //#region ../ai-ollama/src/utils/map-done-reason.ts
1
+ //#region ../../packages/ai-ollama/src/utils/map-done-reason.ts
2
2
  const doneReasonMap = {
3
3
  stop: "stop",
4
4
  length: "length"
@@ -1 +1 @@
1
- {"version":3,"file":"map-done-reason.mjs","names":[],"sources":["../../../../../../../ai-ollama/src/utils/map-done-reason.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"],"mappings":";AAEA,MAAM,gBAA8C;CAClD,MAAM;CACN,QAAQ;AACV;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,KAA8C;CAC1E,OAAO,cAAc,OAAO,OAAO;AACrC"}
1
+ {"version":3,"file":"map-done-reason.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/map-done-reason.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"],"mappings":";AAEA,MAAM,gBAA8C;CAClD,MAAM;CACN,QAAQ;AACV;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,KAA8C;CAC1E,OAAO,cAAc,OAAO,OAAO;AACrC"}
@@ -1,6 +1,6 @@
1
1
  import { InvalidRequestError } from "@warlock.js/ai";
2
2
 
3
- //#region ../ai-ollama/src/utils/to-ollama-messages.ts
3
+ //#region ../../packages/ai-ollama/src/utils/to-ollama-messages.ts
4
4
  /**
5
5
  * Convert vendor-neutral `Message[]` into the Ollama chat message
6
6
  * shape.
@@ -1 +1 @@
1
- {"version":3,"file":"to-ollama-messages.mjs","names":[],"sources":["../../../../../../../ai-ollama/src/utils/to-ollama-messages.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,UAAsC;CACrE,OAAO,SAAS,KAAK,YAA2B;EAC9C,IAAI,QAAQ,SAAS,QACnB,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,WAAW,QAAQ,cAAc;EACnC;EAGF,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAClF,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,YAAY,QAAQ,UAAU,KAAK,cAAc,EAC/C,UAAU;IACR,MAAM,SAAS;IACf,WAAY,SAAS,SAAS,CAAC;GACjC,EACF,EAAE;EACJ;EAGF,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAC1D,OAAO,mBAAmB,QAAQ,OAAO;EAG3C,OAAO;GAAE,MAAM,QAAQ;GAAM,SAAS,iBAAiB,QAAQ,OAAO;EAAE;CAC1E,CAAC;AACH;;;;;;;;;AAUA,SAAS,mBAAmB,OAAqC;CAC/D,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,SAAS,QAAQ;GACxB,WAAW,KAAK,KAAK,IAAI;GAEzB;EACF;EAEA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,6EACF;EAGF,OAAO,KAAK,KAAK,OAAO,MAAM;CAChC;CAEA,OAAO;EACL,MAAM;EACN,SAAS,WAAW,KAAK,EAAE;EAC3B,GAAI,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;CACxC;AACF;;;;;AAMA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ"}
1
+ {"version":3,"file":"to-ollama-messages.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/to-ollama-messages.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,UAAsC;CACrE,OAAO,SAAS,KAAK,YAA2B;EAC9C,IAAI,QAAQ,SAAS,QACnB,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,WAAW,QAAQ,cAAc;EACnC;EAGF,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAClF,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,QAAQ,OAAO;GACzC,YAAY,QAAQ,UAAU,KAAK,cAAc,EAC/C,UAAU;IACR,MAAM,SAAS;IACf,WAAY,SAAS,SAAS,CAAC;GACjC,EACF,EAAE;EACJ;EAGF,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAC1D,OAAO,mBAAmB,QAAQ,OAAO;EAG3C,OAAO;GAAE,MAAM,QAAQ;GAAM,SAAS,iBAAiB,QAAQ,OAAO;EAAE;CAC1E,CAAC;AACH;;;;;;;;;AAUA,SAAS,mBAAmB,OAAqC;CAC/D,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,SAAS,QAAQ;GACxB,WAAW,KAAK,KAAK,IAAI;GAEzB;EACF;EAEA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,6EACF;EAGF,OAAO,KAAK,KAAK,OAAO,MAAM;CAChC;CAEA,OAAO;EACL,MAAM;EACN,SAAS,WAAW,KAAK,EAAE;EAC3B,GAAI,OAAO,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;CACxC;AACF;;;;;AAMA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ"}
@@ -1,6 +1,6 @@
1
1
  import { extractJsonSchema } from "@warlock.js/ai";
2
2
 
3
- //#region ../ai-ollama/src/utils/to-ollama-tools.ts
3
+ //#region ../../packages/ai-ollama/src/utils/to-ollama-tools.ts
4
4
  /**
5
5
  * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.
6
6
  * Each tool becomes a `{ type: "function", function: { name,
@@ -1 +1 @@
1
- {"version":3,"file":"to-ollama-tools.mjs","names":[],"sources":["../../../../../../../ai-ollama/src/utils/to-ollama-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"ollama\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.\n * Each tool becomes a `{ type: \"function\", function: { name,\n * description, parameters } }` entry. Non-object extractions degrade\n * to a parameterless object so registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `tools` from the request.\n *\n * @example\n * const tools = toOllamaTools([weatherTool]);\n * await ollama.chat({ model, messages, tools });\n */\nexport function toOllamaTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Ollama wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Tool[\"function\"][\"parameters\"] {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema as Tool[\"function\"][\"parameters\"];\n }\n\n return { type: \"object\" } as Tool[\"function\"][\"parameters\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;AAOA,SAAS,aAAa,OAA8E;CAClG,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
1
+ {"version":3,"file":"to-ollama-tools.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/to-ollama-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type { Tool } from \"ollama\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` into Ollama's `tools` array.\n * Each tool becomes a `{ type: \"function\", function: { name,\n * description, parameters } }` entry. Non-object extractions degrade\n * to a parameterless object so registration never fails.\n *\n * Returns `undefined` when there are no tools so the caller can omit\n * `tools` from the request.\n *\n * @example\n * const tools = toOllamaTools([weatherTool]);\n * await ollama.chat({ model, messages, tools });\n */\nexport function toOllamaTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): Tool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. Ollama wants\n * an object root for function parameters; anything else (or a failed\n * extraction) degrades to a parameterless object.\n */\nfunction toParameters(\n input: ToolConfig<unknown, unknown>[\"input\"],\n): Tool[\"function\"][\"parameters\"] {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema as Tool[\"function\"][\"parameters\"];\n }\n\n return { type: \"object\" } as Tool[\"function\"][\"parameters\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,cACd,OACoB;CACpB,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;AAOA,SAAS,aACP,OACgC;CAChC,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO,EAAE,MAAM,SAAS;AAC1B"}
@@ -1,6 +1,6 @@
1
1
  import { AIError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError } from "@warlock.js/ai";
2
2
 
3
- //#region ../ai-ollama/src/utils/wrap-ollama-error.ts
3
+ //#region ../../packages/ai-ollama/src/utils/wrap-ollama-error.ts
4
4
  /**
5
5
  * Wrap any thrown value caught inside the Ollama adapter into the
6
6
  * appropriate `@warlock.js/ai` `AIError` subclass.
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-ollama-error.mjs","names":[],"sources":["../../../../../../../ai-ollama/src/utils/wrap-ollama-error.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,gBAAgB,QAA0B;CACxD,IAAI,kBAAkB,SACpB,OAAO;CAGT,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,UAAU,aAAa,KAAK;CAClC,MAAM,UAAU,MAAM,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;CAE1F,IAAI,UAAU,KAAK,GACjB,OAAO,IAAI,qBAAqB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGrE,IAAI,oBAAoB,OAAO,OAAO,GACpC,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAG9D,IAAI,MAAM,eAAe,OAAO,MAAM,eAAe,KACnD,OAAO,IAAI,kBAAkB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGlE,IAAI,MAAM,eAAe,KACvB,OAAO,IAAI,uBAAuB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGvE,IAAI,eAAe,MAAM,UAAU,GAAG;EACpC,IAAI,mDAAmD,KAAK,OAAO,GACjE,OAAO,IAAI,2BAA2B,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;EAG3E,OAAO,IAAI,oBAAoB,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;CACpE;CAEA,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;AAC9D;;;;;;AAOA,SAAS,QAAQ,QAAmC;CAClD,IAAI,OAAO,WAAW,YAAY,WAAW,MAC3C,OAAO,CAAC;CAGV,MAAM,MAAM;CACZ,MAAM,QAAQ,IAAI;CAElB,OAAO;EACL,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAChD,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;EACzD,YAAY,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EACpE,MACE,OAAO,IAAI,SAAS,WAChB,IAAI,OACJ,SAAS,OAAO,MAAM,SAAS,WAC7B,MAAM,OACN;CACV;AACF;;AAGA,SAAS,UAAU,OAAkC;CACnD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,gBAChD,OAAO;CAGT,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS;AACtD;;;;;;AAOA,SAAS,oBAAoB,OAAyB,SAA0B;CAC9E,OAAO,MAAM,SAAS,kBAAkB,6BAA6B,KAAK,OAAO;AACnF;;AAGA,SAAS,eAAe,QAAqC;CAC3D,OAAO,OAAO,WAAW,YAAY,UAAU,OAAO,SAAS;AACjE;;AAGA,SAAS,aAAa,OAAkD;CACtE,MAAM,UAAmC,CAAC;CAE1C,IAAI,MAAM,eAAe,QACvB,QAAQ,SAAS,MAAM;CAGzB,IAAI,MAAM,MACR,QAAQ,OAAO,MAAM;CAGvB,OAAO;AACT"}
1
+ {"version":3,"file":"wrap-ollama-error.mjs","names":[],"sources":["../../../../../../../../packages/ai-ollama/src/utils/wrap-ollama-error.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,gBAAgB,QAA0B;CACxD,IAAI,kBAAkB,SACpB,OAAO;CAGT,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,UAAU,aAAa,KAAK;CAClC,MAAM,UAAU,MAAM,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;CAE1F,IAAI,UAAU,KAAK,GACjB,OAAO,IAAI,qBAAqB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGrE,IAAI,oBAAoB,OAAO,OAAO,GACpC,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAG9D,IAAI,MAAM,eAAe,OAAO,MAAM,eAAe,KACnD,OAAO,IAAI,kBAAkB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGlE,IAAI,MAAM,eAAe,KACvB,OAAO,IAAI,uBAAuB,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;CAGvE,IAAI,eAAe,MAAM,UAAU,GAAG;EACpC,IAAI,mDAAmD,KAAK,OAAO,GACjE,OAAO,IAAI,2BAA2B,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;EAG3E,OAAO,IAAI,oBAAoB,SAAS;GAAE,OAAO;GAAQ;EAAQ,CAAC;CACpE;CAEA,OAAO,IAAI,cAAc,SAAS;EAAE,OAAO;EAAQ;CAAQ,CAAC;AAC9D;;;;;;AAOA,SAAS,QAAQ,QAAmC;CAClD,IAAI,OAAO,WAAW,YAAY,WAAW,MAC3C,OAAO,CAAC;CAGV,MAAM,MAAM;CACZ,MAAM,QAAQ,IAAI;CAElB,OAAO;EACL,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;EAChD,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;EACzD,YAAY,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EACpE,MACE,OAAO,IAAI,SAAS,WAChB,IAAI,OACJ,SAAS,OAAO,MAAM,SAAS,WAC7B,MAAM,OACN;CACV;AACF;;AAGA,SAAS,UAAU,OAAkC;CACnD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,gBAChD,OAAO;CAGT,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS;AACtD;;;;;;AAOA,SAAS,oBAAoB,OAAyB,SAA0B;CAC9E,OAAO,MAAM,SAAS,kBAAkB,6BAA6B,KAAK,OAAO;AACnF;;AAGA,SAAS,eAAe,QAAqC;CAC3D,OAAO,OAAO,WAAW,YAAY,UAAU,OAAO,SAAS;AACjE;;AAGA,SAAS,aAAa,OAAkD;CACtE,MAAM,UAAmC,CAAC;CAE1C,IAAI,MAAM,eAAe,QACvB,QAAQ,SAAS,MAAM;CAGzB,IAAI,MAAM,MACR,QAAQ,OAAO,MAAM;CAGvB,OAAO;AACT"}