/** * Nano Banana / Gemini Native Image Generation Adapter * * Uses Google Gemini's native image generation capability. * Endpoint: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent * * Supported models: * - gemini-3.1-flash-image-preview (Nano Banana 2 — latest, fastest) * - gemini-3-pro-image-preview (Nano Banana Pro — highest quality) * - gemini-2.5-flash-image (Nano Banana — original) * * Authentication: x-goog-api-key header * * API docs: https://ai.google.dev/gemini-api/docs/image-generation */ import type { ImageGenerationConfig, ImageGenerationOptions, ImageGenerationResult, } from '../types'; import { requireModel } from '../require-model'; const DEFAULT_MODEL = 'gemini-2.5-flash-image'; const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com'; interface GeminiPart { text?: string; inlineData?: { mimeType: string; data: string; }; } interface GeminiResponse { candidates?: Array<{ content?: { parts?: GeminiPart[]; }; }>; error?: { code: number; message: string; status: string; }; } /** * Lightweight connectivity test — validates API key by fetching model info. * Uses GET /v1beta/models/{model} which does not trigger generation. */ export async function testNanoBananaConnectivity( config: ImageGenerationConfig, ): Promise<{ success: boolean; message: string }> { const baseUrl = config.baseUrl || DEFAULT_BASE_URL; const model = config.model || DEFAULT_MODEL; const url = `${baseUrl}/v1beta/models`; // Try ?key= query param first (direct Google API), fall back to x-goog-api-key header (proxy) let response: Response | null = null; try { response = await fetch(`${url}?key=${config.apiKey}`, { method: 'GET', redirect: 'manual', }); } catch { // Direct API unreachable, try header auth } if (!response || !response.ok) { try { response = await fetch(url, { method: 'GET', redirect: 'manual', headers: { 'x-goog-api-key': config.apiKey }, }); } catch (_err) { return { success: false, message: `Network error: unable to reach ${baseUrl}. Check your Base URL and network connection.`, }; } } if (response.ok) { return { success: true, message: `Connected to Nano Banana (${model})` }; } // Parse error body for user-friendly message const text = await response.text().catch(() => ''); if (response.status === 400 || response.status === 401 || response.status === 403) { return { success: false, message: `Invalid API key or unauthorized (${response.status}). Check your API Key and Base URL match the same provider.`, }; } return { success: false, message: `Nano Banana connectivity failed (${response.status}): ${text}`, }; } export async function generateWithNanoBanana( config: ImageGenerationConfig, options: ImageGenerationOptions, ): Promise { const baseUrl = config.baseUrl || DEFAULT_BASE_URL; const model = requireModel(config.model, 'Nano Banana'); const response = await fetch(`${baseUrl}/v1beta/models/${model}:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': config.apiKey, }, body: JSON.stringify({ contents: [ { parts: [{ text: options.prompt }], }, ], generationConfig: { responseModalities: ['IMAGE'], }, }), }); if (!response.ok) { const text = await response.text(); throw new Error(`Gemini image generation failed (${response.status}): ${text}`); } const data: GeminiResponse = await response.json(); if (data.error) { throw new Error(`Gemini error: ${data.error.code} - ${data.error.message}`); } const parts = data.candidates?.[0]?.content?.parts; if (!parts || parts.length === 0) { throw new Error('Gemini returned empty response'); } // Find the image part (inlineData with base64) const imagePart = parts.find((p) => p.inlineData); if (!imagePart?.inlineData) { // Might have returned text only (e.g. if prompt was rejected) const textPart = parts.find((p) => p.text); throw new Error(`Gemini did not return an image. Response text: ${textPart?.text || 'none'}`); } return { base64: imagePart.inlineData.data, width: options.width || 1024, height: options.height || 1024, }; }