← back to AbramsOS
lib/ollama.js
60 lines
// Local Ollama client. Mac1 is the bulk default; Mac2 is overflow.
// Per Steve's rules: NEVER call the Anthropic API. Local Ollama only.
const DEFAULT_BASE = process.env.OLLAMA_BASE_URL || 'http://192.168.1.133:11434';
const DEFAULT_MODEL = process.env.OLLAMA_MODEL || 'qwen3:14b';
const DEFAULT_TIMEOUT_MS = 30_000;
async function generate(prompt, { model = DEFAULT_MODEL, base = DEFAULT_BASE, timeoutMs = DEFAULT_TIMEOUT_MS, format = null, system = null } = {}) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const body = { model, prompt, stream: false };
if (format) body.format = format;
if (system) body.system = system;
const r = await fetch(`${base}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: ctrl.signal,
});
if (!r.ok) throw new Error(`ollama ${r.status}: ${await r.text().catch(() => '')}`);
const j = await r.json();
return (j.response || '').trim();
} finally {
clearTimeout(t);
}
}
async function generateJson(prompt, opts = {}) {
// Use Ollama's structured-output mode by passing format: "json".
const raw = await generate(prompt, { ...opts, format: 'json' });
// Some models still wrap in code fences; strip if present.
const cleaned = raw.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
try {
return JSON.parse(cleaned);
} catch (err) {
// Last-resort: pull first {...} block
const m = cleaned.match(/\{[\s\S]*\}/);
if (m) {
try { return JSON.parse(m[0]); } catch (_) {}
}
throw new Error(`ollama returned non-JSON: ${cleaned.slice(0, 200)}`);
}
}
async function reachable(base = DEFAULT_BASE, timeoutMs = 3000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const r = await fetch(`${base}/api/tags`, { signal: ctrl.signal });
return r.ok;
} catch (_) {
return false;
} finally {
clearTimeout(t);
}
}
module.exports = { generate, generateJson, reachable, DEFAULT_BASE, DEFAULT_MODEL };