← back to Wallco Ai

lib/gemini.js

74 lines

// Gemini API wrapper — consolidates the 3 scattered fetch+key+logGemini
// call sites previously duplicated in server.js (crop-fix, design-critique,
// ai-designer). Centralizes:
//   - key resolution (GOOGLE_API_KEY → GEMINI_API_KEY)
//   - URL build (one place to bump API version)
//   - cost-tracker logGemini accounting (was try/catch-wrapped at every site)
//   - response shape probing (handles both inlineData and inline_data)
//
// Errors throw with `.code` set: NO_KEY | HTTP_ERROR (+.status) | NETWORK.
// Caller decides response status code from .code/.status.

const path = require('path');
const os   = require('os');

function geminiKey() {
  return (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || '').trim();
}

async function geminiCall({ model, parts, generationConfig, app = 'wallco-ai', note = '' }) {
  const key = geminiKey();
  if (!key) {
    const err = new Error('GEMINI_API_KEY not configured');
    err.code = 'NO_KEY';
    throw err;
  }
  const url  = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
  const body = { contents: [{ parts }] };
  if (generationConfig) body.generationConfig = generationConfig;

  let r;
  try {
    r = await fetch(url, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(body),
    });
  } catch (e) {
    const err = new Error('gemini network: ' + e.message);
    err.code = 'NETWORK';
    throw err;
  }
  if (!r.ok) {
    const text = (await r.text()).slice(0, 240);
    const err  = new Error(`gemini http ${r.status}: ${text}`);
    err.code   = 'HTTP_ERROR';
    err.status = r.status;
    throw err;
  }
  const j = await r.json();
  try {
    const { logGemini } = require(path.join(os.homedir(), '.claude/skills/cost-tracker/scripts/log-gemini.js'));
    logGemini(j, { app, model, note });
  } catch { /* non-fatal */ }
  return j;
}

function extractText(j) {
  return j?.candidates?.[0]?.content?.parts?.find(p => p.text)?.text || null;
}

function extractImageData(j) {
  const part = j?.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
  return part?.inline_data?.data || part?.inlineData?.data || null;
}

function extractJson(j) {
  const t = extractText(j);
  if (!t) return null;
  try { return JSON.parse(t.replace(/^```json\s*|\s*```$/g, '')); }
  catch { return null; }
}

module.exports = { geminiKey, geminiCall, extractText, extractImageData, extractJson };