← back to PoppyPetitions
lib/gemini.ts
67 lines
// 2026-05-04 (architect-reviewer P0 leak): hardcoded Gemini API key was
// committed at line 1 cleartext. Scrubbed. The literal value is COMPROMISED
// and MUST be rotated via /secrets — do not assume git history is clean.
// Mint a fresh key at https://aistudio.google.com/apikey, route via
// secrets-manager to all consumers (this file, Grant, future siblings).
const GEMINI_KEY = process.env.GEMINI_API_KEY;
if (!GEMINI_KEY) {
// Fail-fast at module load so a missing-key deployment crashes loudly
// instead of returning silent 500s on every Gemini call.
console.error('[lib/gemini] GEMINI_API_KEY env unset — Gemini calls will fail');
}
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY ?? ''}`;
export interface GeminiResponse {
text: string;
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
/**
* Call Gemini 2.0 Flash and return the generated text + token counts.
*/
export async function callGemini(prompt: string): Promise<GeminiResponse> {
if (!GEMINI_KEY) {
throw new Error('GEMINI_API_KEY env unset on server');
}
// 2026-05-04 (architect-reviewer): match the 25s timeout pattern used in
// Grant. Without this, hung Gemini calls hold pg pool slots indefinitely.
const res = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.8,
maxOutputTokens: 2048,
},
}),
signal: AbortSignal.timeout(25_000),
});
if (!res.ok) {
const errBody = await res.text();
throw new Error(`Gemini API error ${res.status}: ${errBody}`);
}
const data = await res.json();
const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
const usage = data.usageMetadata ?? {};
return {
text,
inputTokens: usage.promptTokenCount ?? 0,
outputTokens: usage.candidatesTokenCount ?? 0,
totalTokens: usage.totalTokenCount ?? 0,
};
}
/**
* Estimate cost based on Gemini 2.0 Flash pricing.
* Input: $0.10 per 1M tokens, Output: $0.40 per 1M tokens
*/
export function estimateCost(inputTokens: number, outputTokens: number): number {
return (inputTokens / 1_000_000) * 0.10 + (outputTokens / 1_000_000) * 0.40;
}