/** * OllamaProvider — local models over Ollama's native `/api/chat`. * * Pattern: Adapter (GoF) + Ports-and-Adapters (Cockburn 2005). * Role: Outer ring — translates `LLMRequest`/`LLMResponse` to/from * Ollama's native chat wire. Knows nothing about agents, * recorders, or compositions. * Emits: N/A. * * ─── Why this exists, and why it talks the native wire ─────────────── * * The adapter ladder is `mock()` → a local model → a paid API, and the * strongest version of "the test run and the production run are the same * code path" is one where the middle step costs nothing and needs no API * key. Through 8.0.0 the middle step needed BOTH a competitor's SDK * (`npm install openai`, because `ollama()` was a thin wrapper over * `openai({ baseURL })`) and a tolerance for failures labelled `[openai]`. * That is not a free rung. * * So this file owns the wire: * * • ZERO dependencies — one `fetch` POST and NDJSON. Nothing to install * beyond Ollama itself. (Same choice, same reasons, as the two * `Browser*Provider` adapters.) * • HONEST REFUSALS — a typed {@link OllamaUnavailableError} that names * the address it tried and the command to run. Owning the fetch and * the status code is what makes that possible; through an SDK you get * whatever the SDK decided to throw. * • REAL TOKEN COUNTS — `/api/chat` returns `prompt_eval_count` / * `eval_count` on every response, streaming or not, with no opt-in * flag. Through the OpenAI-compatible endpoint a streamed local call * reported ZERO tokens, which silently disarmed `.compaction()` and * `costBudget` (see `CompactionUnmeasurableError`, whose message names * this exact case). * • STRUCTURED THINKING — `message.thinking` is a first-class field on * this wire; the OpenAI-compatible layer renames it to a non-standard * `reasoning` key that no OpenAI adapter reads. See * `OllamaThinkingHandler`. * * Want the SDK path instead? It is still there and still supported: * `openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' })`. * * ─── Ceilings (stated, not worked around) ──────────────────────────── * * • TOOL CALLING IS MODEL-DEPENDENT. Ollama forwards a `tools` array to * any model; a model that was not trained for tools simply answers in * prose and no tool call ever arrives. This adapter does not preflight * `/api/show` capabilities to refuse first — a wrong refusal is worse * than a weak answer, and the metadata is less reliable than the * ceiling being written down. Pick a tool-capable model. * • NO FORCED TOOL CHOICE. Ollama does not support `tool_choice` on * either wire, so `carriesForcedToolChoice` is `false` and an agent * using `.outputSchema(parser, { strategy: 'tool-forced' })` refuses at * run start, naming this provider. * • NO MULTI-MODAL. The wire carries `images`; `LLMMessage.content` is a * string. Same ceiling as every other adapter here. * • NO PROMPT CACHING — resolves to the NoOp cache strategy. * • NO `providerRef`. The native wire returns no response id, and a * fabricated one would point at nothing. */ import type { LLMCallHooks, LLMChunk, LLMProvider, LLMRequest, LLMResponse, WireRole } from '../types.js'; /** How hard a thinking model should think. Ollama's own vocabulary. */ export type ThinkLevel = 'low' | 'medium' | 'high' | 'max'; /** * The two failures a local runtime actually has, told in words that * contain the fix. * * Both are things the person at the keyboard can resolve in one command, * which is exactly why they get a type instead of a wrapped * `ECONNREFUSED` or a bare `404`. `reason` is the discriminator; the * message already reads as instructions. */ export declare class OllamaUnavailableError extends Error { readonly name = "OllamaUnavailableError"; /** Which of the two situations this is. */ readonly reason: 'daemon-unreachable' | 'model-not-pulled'; /** The address that was tried — the thing to check or change. */ readonly baseUrl: string; /** The model asked for. Absent when the daemon never answered at all. */ readonly model?: string; /** Models this machine DOES have, when the daemon could tell us. */ readonly availableModels?: readonly string[]; constructor(init: { reason: 'daemon-unreachable' | 'model-not-pulled'; baseUrl: string; model?: string; availableModels?: readonly string[]; cause?: unknown; }); } export interface OllamaProviderOptions { /** * Where Ollama is listening. Defaults to `OLLAMA_HOST` when set, * otherwise `http://localhost:11434`. A bare `host:port` gets `http://`. * * Point it at the ROOT, not at a path — this adapter talks to * `/api/chat` and `/api/tags` itself. A URL ending in `/v1` (the * OpenAI-compatible path) is accepted and trimmed, so a config written * for the 8.0.0 factory keeps working. */ readonly baseUrl?: string; /** Shipped-in-8.0.0 spelling of {@link baseUrl}. Still honored. */ readonly host?: string; /** Shipped-in-8.0.0 spelling of {@link baseUrl}. Still honored. */ readonly baseURL?: string; /** * Model used when `LLMRequest.model` is the `'ollama'` shorthand. * Prefer the positional form: `ollama('qwen3')`. */ readonly defaultModel?: string; /** Default token cap when the request doesn't set one. Maps to `num_predict`. */ readonly defaultMaxTokens?: number; /** * Ask a thinking model to reason before answering. `true` turns it on; * `'low' | 'medium' | 'high' | 'max'` sets how hard. * * Worth setting on reasoning models (deepseek-r1, qwen3, gpt-oss, …): * with `think` on, Ollama lifts the reasoning OUT of the answer into * `message.thinking`, where `ollamaThinkingHandler` normalizes it. * With it off, the same model leaves `` sitting in the * answer text. * * A per-request `LLMRequest.thinking` (what `AgentBuilder.thinking()` * sets) also turns it on; this option is the always-on default and the * only way to name a level. */ readonly think?: boolean | ThinkLevel; /** How long Ollama keeps the model in memory after a call, e.g. `'5m'`. */ readonly keepAlive?: string | number; /** * How long to wait for the daemon to ANSWER, in ms. Default 10000. * * This bounds the wait for response headers, NOT generation: a laptop * model may take minutes to finish a long answer and that is fine. What * it prevents is the failure this whole file exists to avoid — a run * that hangs because nothing is listening. */ readonly timeoutMs?: number; /** Accepted and ignored — Ollama needs no key. Kept so 8.0.0 configs still typecheck. */ readonly apiKey?: string; /** @internal Custom fetch implementation for tests. */ readonly _fetch?: typeof fetch; } /** * Build an `LLMProvider` backed by a local Ollama runtime. * * Free, offline, no API key. The rung between `mock()` and a paid API — * and the agent code above it does not change between the three. * * @example * import { Agent } from 'agentfootprint'; * import { ollama } from 'agentfootprint/providers'; * * const agent = Agent.create({ * provider: ollama('qwen3'), * model: 'qwen3', * }).build(); * * @example // reasoning model, thinking lifted out of the answer * ollama('deepseek-r1', { think: 'high' }); * * @example // a runtime on another machine * ollama('llama3.2', { baseUrl: 'http://192.168.1.20:11434' }); */ export declare function ollama(model: string, options?: OllamaProviderOptions): LLMProvider; /** * Object form, as shipped in 8.0.0. Still supported — `host` / `baseURL` / * `defaultModel` / `apiKey` all keep their meaning. */ export declare function ollama(options?: OllamaProviderOptions): LLMProvider; /** * Class form for consumers who prefer `new OllamaProvider(...)`. */ export declare class OllamaProvider implements LLMProvider { readonly name = "ollama"; readonly carriesInMessages: readonly WireRole[]; readonly carriesForcedToolChoice = false; private readonly inner; constructor(model?: string | OllamaProviderOptions, options?: OllamaProviderOptions); complete(req: LLMRequest, hooks?: LLMCallHooks): Promise; stream(req: LLMRequest, hooks?: LLMCallHooks): AsyncIterable; } //# sourceMappingURL=OllamaProvider.d.ts.map