← back to AbramsOS
lib/chat-providers/ollama.js
56 lines
// Ollama adapter — local, $0, on-box. Private lane: raw PII is allowed here
// because nothing leaves the network. Degrades gracefully when a model is not
// pulled.
const BASE = process.env.OLLAMA_BASE_URL || 'http://192.168.1.133:11434';
const CHAT_TIMEOUT_MS = 60_000;
const TAGS_TIMEOUT_MS = 3_000;
async function availableModels(base = BASE) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), TAGS_TIMEOUT_MS);
try {
const r = await fetch(`${base}/api/tags`, { signal: ctrl.signal });
if (!r.ok) return [];
const j = await r.json();
return (j.models || []).map((m) => m.name);
} catch (_) {
return [];
} finally {
clearTimeout(t);
}
}
async function chat({ system, messages, providerModel }) {
const models = await availableModels();
if (models.length && !models.includes(providerModel)) {
throw new Error(`model ${providerModel} not pulled on Ollama`);
}
const msgs = [];
if (system) msgs.push({ role: 'system', content: String(system) });
for (const m of messages) {
msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', content: String(m.content ?? '') });
}
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), CHAT_TIMEOUT_MS);
try {
const r = await fetch(`${BASE}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: providerModel, messages: msgs, stream: false }),
signal: ctrl.signal,
});
if (!r.ok) throw new Error(`ollama ${r.status}: ${(await r.text().catch(() => '')).slice(0, 300)}`);
const j = await r.json();
const text = (j.message?.content || '').trim();
if (!text) throw new Error('ollama returned empty response');
return { text };
} finally {
clearTimeout(t);
}
}
module.exports = { chat, availableModels, BASE };