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