← 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 };