feat: switch to RunPod proxy for Gemini image generation

- Use RunPod serverless endpoint (US-based) to bypass EU geo-restrictions
- Add RUNPOD_API_KEY and RUNPOD_GEMINI_ENDPOINT_ID env vars
- Add fallback to direct Gemini API if RunPod not configured
- Endpoint ID: ntqjz8cdsth42i

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-18 22:10:13 -05:00
parent e5a7b67927
commit 1aec028c52
2 changed files with 82 additions and 26 deletions

View File

@ -11,7 +11,8 @@ services:
environment:
- NODE_ENV=production
- GEMINI_API_KEY=${GEMINI_API_KEY}
- GEMINI_PROXY_URL=https://gemini-proxy.jeffemmett.workers.dev
- RUNPOD_API_KEY=${RUNPOD_API_KEY}
- RUNPOD_GEMINI_ENDPOINT_ID=ntqjz8cdsth42i
- NEXT_PUBLIC_APP_URL=https://zine.jeffemmett.com
- DATA_DIR=/app/data
volumes:

View File

@ -123,18 +123,29 @@ async function generateImageWithGemini(
}
// Gemini 2.0 Flash with native image generation (Nano Banana)
// Uses Cloudflare Worker proxy to bypass geo-restrictions
// Uses RunPod serverless proxy (US-based) to bypass geo-restrictions
async function generateWithGemini2FlashImage(prompt: string, apiKey: string): Promise<string | null> {
// Use the Cloudflare Worker proxy to route through US
const proxyUrl = process.env.GEMINI_PROXY_URL || "https://gemini-proxy.jeffemmett.workers.dev";
// Use RunPod serverless endpoint (US-based) to bypass geo-restrictions
const runpodEndpointId = process.env.RUNPOD_GEMINI_ENDPOINT_ID || "ntqjz8cdsth42i";
const runpodApiKey = process.env.RUNPOD_API_KEY;
const response = await fetch(proxyUrl, {
if (!runpodApiKey) {
console.error("RUNPOD_API_KEY not configured, falling back to direct API");
return generateDirectGeminiImage(prompt, apiKey);
}
const runpodUrl = `https://api.runpod.ai/v2/${runpodEndpointId}/runsync`;
try {
const response = await fetch(runpodUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
"Authorization": `Bearer ${runpodApiKey}`,
},
body: JSON.stringify({
input: {
api_key: apiKey,
model: "gemini-2.0-flash-exp",
contents: [
{
@ -148,24 +159,69 @@ async function generateWithGemini2FlashImage(prompt: string, apiKey: string): Pr
generationConfig: {
responseModalities: ["TEXT", "IMAGE"],
},
},
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error("Gemini proxy API error:", response.status, errorText);
console.error("RunPod API error:", response.status, errorText);
return null;
}
const result = await response.json();
// RunPod wraps the response in { output: ... }
const data = result.output || result;
// Check for errors
if (data.error) {
console.error("Gemini API error via RunPod:", data.error);
return null;
}
// Extract image from Gemini response
const parts = data.candidates?.[0]?.content?.parts || [];
for (const part of parts) {
if (part.inlineData?.mimeType?.startsWith("image/")) {
console.log("✅ Generated image via RunPod proxy");
return part.inlineData.data;
}
}
console.error("No image in Gemini response via RunPod, parts:", JSON.stringify(parts).slice(0, 500));
return null;
} catch (error) {
console.error("RunPod request error:", error);
return null;
}
}
// Fallback: Try direct Gemini API (will fail in geo-restricted regions)
async function generateDirectGeminiImage(prompt: string, apiKey: string): Promise<string | null> {
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`;
const response = await fetch(geminiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ parts: [{ text: `Generate an image: ${prompt}` }] }],
generationConfig: { responseModalities: ["TEXT", "IMAGE"] },
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error("Direct Gemini API error:", response.status, errorText);
return null;
}
const data = await response.json();
// Check for API errors in response
if (data.error) {
console.error("Gemini API error via proxy:", data.error);
console.error("Gemini API error:", data.error);
return null;
}
// Extract image from response
const parts = data.candidates?.[0]?.content?.parts || [];
for (const part of parts) {
if (part.inlineData?.mimeType?.startsWith("image/")) {
@ -173,7 +229,6 @@ async function generateWithGemini2FlashImage(prompt: string, apiKey: string): Pr
}
}
console.error("No image in Gemini response, parts:", JSON.stringify(parts).slice(0, 500));
return null;
}