const DEFAULT_URL = "http://localhost:11434"; export class Ollama { constructor({ url = DEFAULT_URL, timeoutMs = 600000 } = {}) { this.url = url.replace(/\/$/, ""); this.timeoutMs = timeoutMs; } async ping({ timeoutMs = 30000 } = {}) { if (process.env.OLLAMA_SKIP_PING === "1") return { ok: true, model: process.env.OLLAMA_MODEL }; const res = await fetch(`${this.url}/api/tags`, { signal: AbortSignal.timeout(timeoutMs) }); if (!res.ok) throw new Error(`Ollama not reachable (HTTP ${res.status}) at ${this.url}`); const data = await res.json(); const models = (data.models ?? []).map((m) => m.name); return { ok: true, models }; } async chat({ model, messages, tools, temperature = 0, numCtx = 8192, timeoutMs }) { const body = { model, messages, stream: false, options: { temperature }, }; if (numCtx) body.options.num_ctx = numCtx; if (tools && tools.length) body.tools = tools; const res = await fetch(`${this.url}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs ?? this.timeoutMs), }); if (!res.ok) { const t = await res.text(); throw new Error(`Ollama chat HTTP ${res.status}: ${t.slice(0, 300)}`); } const data = await res.json(); const message = data.message ?? {}; return { role: message.role ?? "assistant", content: message.content ?? "", toolCalls: message.tool_calls ?? [], promptEvalCount: data.prompt_eval_count ?? 0, evalCount: data.eval_count ?? 0, raw: data, }; } } export async function listModels() { const o = new Ollama(); const res = await fetch(`${o.url}/api/tags`); if (!res.ok) throw new Error(`Ollama not reachable (HTTP ${res.status})`); const data = await res.json(); return (data.models ?? []).map((m) => m.name); }