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