nano-banana-adapter.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /**
  2. * Nano Banana / Gemini Native Image Generation Adapter
  3. *
  4. * Uses Google Gemini's native image generation capability.
  5. * Endpoint: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
  6. *
  7. * Supported models:
  8. * - gemini-3.1-flash-image-preview (Nano Banana 2 — latest, fastest)
  9. * - gemini-3-pro-image-preview (Nano Banana Pro — highest quality)
  10. * - gemini-2.5-flash-image (Nano Banana — original)
  11. *
  12. * Authentication: x-goog-api-key header
  13. *
  14. * API docs: https://ai.google.dev/gemini-api/docs/image-generation
  15. */
  16. import type {
  17. ImageGenerationConfig,
  18. ImageGenerationOptions,
  19. ImageGenerationResult,
  20. } from '../types';
  21. const DEFAULT_MODEL = 'gemini-2.5-flash-image';
  22. const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
  23. interface GeminiPart {
  24. text?: string;
  25. inlineData?: {
  26. mimeType: string;
  27. data: string;
  28. };
  29. }
  30. interface GeminiResponse {
  31. candidates?: Array<{
  32. content?: {
  33. parts?: GeminiPart[];
  34. };
  35. }>;
  36. error?: {
  37. code: number;
  38. message: string;
  39. status: string;
  40. };
  41. }
  42. /**
  43. * Lightweight connectivity test — validates API key by fetching model info.
  44. * Uses GET /v1beta/models/{model} which does not trigger generation.
  45. */
  46. export async function testNanoBananaConnectivity(
  47. config: ImageGenerationConfig,
  48. ): Promise<{ success: boolean; message: string }> {
  49. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  50. const model = config.model || DEFAULT_MODEL;
  51. const url = `${baseUrl}/v1beta/models`;
  52. // Try ?key= query param first (direct Google API), fall back to x-goog-api-key header (proxy)
  53. let response: Response | null = null;
  54. try {
  55. response = await fetch(`${url}?key=${config.apiKey}`, { method: 'GET' });
  56. } catch {
  57. // Direct API unreachable, try header auth
  58. }
  59. if (!response || !response.ok) {
  60. try {
  61. response = await fetch(url, {
  62. method: 'GET',
  63. headers: { 'x-goog-api-key': config.apiKey },
  64. });
  65. } catch (_err) {
  66. return {
  67. success: false,
  68. message: `Network error: unable to reach ${baseUrl}. Check your Base URL and network connection.`,
  69. };
  70. }
  71. }
  72. if (response.ok) {
  73. return { success: true, message: `Connected to Nano Banana (${model})` };
  74. }
  75. // Parse error body for user-friendly message
  76. const text = await response.text().catch(() => '');
  77. if (response.status === 400 || response.status === 401 || response.status === 403) {
  78. return {
  79. success: false,
  80. message: `Invalid API key or unauthorized (${response.status}). Check your API Key and Base URL match the same provider.`,
  81. };
  82. }
  83. return {
  84. success: false,
  85. message: `Nano Banana connectivity failed (${response.status}): ${text}`,
  86. };
  87. }
  88. export async function generateWithNanoBanana(
  89. config: ImageGenerationConfig,
  90. options: ImageGenerationOptions,
  91. ): Promise<ImageGenerationResult> {
  92. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  93. const model = config.model || DEFAULT_MODEL;
  94. const response = await fetch(`${baseUrl}/v1beta/models/${model}:generateContent`, {
  95. method: 'POST',
  96. headers: {
  97. 'Content-Type': 'application/json',
  98. 'x-goog-api-key': config.apiKey,
  99. },
  100. body: JSON.stringify({
  101. contents: [
  102. {
  103. parts: [{ text: options.prompt }],
  104. },
  105. ],
  106. generationConfig: {
  107. responseModalities: ['IMAGE'],
  108. },
  109. }),
  110. });
  111. if (!response.ok) {
  112. const text = await response.text();
  113. throw new Error(`Gemini image generation failed (${response.status}): ${text}`);
  114. }
  115. const data: GeminiResponse = await response.json();
  116. if (data.error) {
  117. throw new Error(`Gemini error: ${data.error.code} - ${data.error.message}`);
  118. }
  119. const parts = data.candidates?.[0]?.content?.parts;
  120. if (!parts || parts.length === 0) {
  121. throw new Error('Gemini returned empty response');
  122. }
  123. // Find the image part (inlineData with base64)
  124. const imagePart = parts.find((p) => p.inlineData);
  125. if (!imagePart?.inlineData) {
  126. // Might have returned text only (e.g. if prompt was rejected)
  127. const textPart = parts.find((p) => p.text);
  128. throw new Error(`Gemini did not return an image. Response text: ${textPart?.text || 'none'}`);
  129. }
  130. return {
  131. base64: imagePart.inlineData.data,
  132. width: options.width || 1024,
  133. height: options.height || 1024,
  134. };
  135. }