← back to AbramsOS
lib/chat-providers/gemini.js
50 lines
// Gemini adapter — Google Generative Language API. External lane: callers MUST
// have already redacted PII from system + messages before reaching here.
const API = 'https://generativelanguage.googleapis.com/v1beta/models';
const TIMEOUT_MS = 30_000;
function toContents(messages) {
return messages.map((m) => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{ text: String(m.content ?? '') }],
}));
}
async function chat({ system, messages, providerModel }) {
const key = process.env.GEMINI_API_KEY;
if (!key) throw new Error('GEMINI_API_KEY not set');
const body = { contents: toContents(messages) };
if (system) body.systemInstruction = { parts: [{ text: String(system) }] };
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
try {
const r = await fetch(`${API}/${encodeURIComponent(providerModel)}:generateContent?key=${key}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: ctrl.signal,
});
if (!r.ok) {
const detail = await r.text().catch(() => '');
throw new Error(`gemini ${r.status}: ${detail.slice(0, 300)}`);
}
const j = await r.json();
const text = (j.candidates?.[0]?.content?.parts || [])
.map((p) => p.text || '')
.join('')
.trim();
if (!text) {
const reason = j.candidates?.[0]?.finishReason || j.promptFeedback?.blockReason || 'empty';
throw new Error(`gemini returned no text (${reason})`);
}
return { text };
} finally {
clearTimeout(t);
}
}
module.exports = { chat };