Files
OmniRoute/open-sse/handlers/imageGeneration.js
diegosouzapw c6000ecc8e feat(providers): add new endpoints (rerank, audio, moderations) and providers (Hyperbolic, Deepgram, AssemblyAI, NanoBanana)
- Add /v1/rerank endpoint with Cohere, Together, NVIDIA, Fireworks
- Add /v1/audio/transcriptions with OpenAI, Groq, Deepgram, AssemblyAI
- Add /v1/audio/speech with OpenAI, Hyperbolic, Deepgram
- Add /v1/moderations with OpenAI
- Add Hyperbolic as chat provider (8 models, OpenAI-compatible)
- Add Hyperbolic image generation (SDXL, SD2, FLUX)
- Add NanoBanana image generation (Flash + Pro via nanobananaapi.ai)
- Add Deepgram STT (Nova 3, Nova 2) with Token auth and binary format
- Add AssemblyAI STT (Universal 3 Pro) with async upload-poll workflow
- Add Deepgram TTS (Aura voices) and Hyperbolic TTS (Melo)
- Update EndpointPageClient to show 7 endpoint sections
- Update /v1/models to return type/subtype for all model categories
- Fix build: remove output:standalone from next.config.mjs
2026-02-13 22:57:32 -03:00

563 lines
15 KiB
JavaScript

/**
* Image Generation Handler
*
* Handles POST /v1/images/generations requests.
* Proxies to upstream image generation providers using OpenAI-compatible format.
*
* Request format (OpenAI-compatible):
* {
* "model": "openai/dall-e-3",
* "prompt": "a beautiful sunset over mountains",
* "n": 1,
* "size": "1024x1024",
* "quality": "standard", // optional: "standard" | "hd"
* "response_format": "url" // optional: "url" | "b64_json"
* }
*/
import { getImageProvider, parseImageModel } from "../config/imageRegistry.js";
import { saveCallLog } from "@/lib/usageDb.js";
/**
* Handle image generation request
* @param {object} options
* @param {object} options.body - Request body
* @param {object} options.credentials - Provider credentials { apiKey, accessToken }
* @param {object} options.log - Logger
*/
export async function handleImageGeneration({ body, credentials, log }) {
const { provider, model } = parseImageModel(body.model);
if (!provider) {
return {
success: false,
status: 400,
error: `Invalid image model: ${body.model}. Use format: provider/model`,
};
}
const providerConfig = getImageProvider(provider);
if (!providerConfig) {
return {
success: false,
status: 400,
error: `Unknown image provider: ${provider}`,
};
}
// Route to format-specific handler
if (providerConfig.format === "gemini-image") {
return handleGeminiImageGeneration({ model, providerConfig, body, credentials, log });
}
if (providerConfig.format === "hyperbolic") {
return handleHyperbolicImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "nanobanana") {
return handleNanoBananaImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log });
}
/**
* Handle Gemini-format image generation (Antigravity / Nano Banana)
* Uses Gemini's generateContent API with responseModalities: ["TEXT", "IMAGE"]
*/
async function handleGeminiImageGeneration({ model, providerConfig, body, credentials, log }) {
const startTime = Date.now();
const url = `${providerConfig.baseUrl}/${model}:generateContent`;
const provider = "antigravity";
// Summarized request for call log
const logRequestBody = {
model: body.model,
prompt:
typeof body.prompt === "string"
? body.prompt.slice(0, 200)
: String(body.prompt ?? "").slice(0, 200),
size: body.size || "default",
n: body.n || 1,
};
const geminiBody = {
contents: [
{
parts: [{ text: body.prompt }],
},
],
generationConfig: {
responseModalities: ["TEXT", "IMAGE"],
},
};
const token = credentials.accessToken || credentials.apiKey;
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
if (log) {
const promptPreview =
typeof body.prompt === "string"
? body.prompt.slice(0, 60)
: String(body.prompt ?? "").slice(0, 60);
log.info(
"IMAGE",
`antigravity/${model} (gemini) | prompt: "${promptPreview}..." | format: gemini-image`
);
}
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(geminiBody),
});
if (!response.ok) {
const errorText = await response.text();
if (log) {
log.error("IMAGE", `antigravity error ${response.status}: ${errorText.slice(0, 200)}`);
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `antigravity/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
requestBody: logRequestBody,
}).catch(() => {});
return { success: false, status: response.status, error: errorText };
}
const data = await response.json();
// Extract image data from Gemini response
const images = [];
const candidates = data.candidates || [];
for (const candidate of candidates) {
const parts = candidate.content?.parts || [];
for (const part of parts) {
if (part.inlineData) {
images.push({
b64_json: part.inlineData.data,
revised_prompt: parts.find((p) => p.text)?.text || body.prompt,
});
}
}
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `antigravity/${model}`,
provider,
duration: Date.now() - startTime,
tokens: { prompt_tokens: 0, completion_tokens: 0 },
requestBody: logRequestBody,
responseBody: { images_count: images.length },
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: images,
},
};
} catch (err) {
if (log) {
log.error("IMAGE", `antigravity fetch error: ${err.message}`);
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `antigravity/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
requestBody: logRequestBody,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}
/**
* Handle OpenAI-compatible image generation (standard providers + Nebius fallback)
*/
async function handleOpenAIImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
// Summarized request for call log
const logRequestBody = {
model: body.model,
prompt:
typeof body.prompt === "string"
? body.prompt.slice(0, 200)
: String(body.prompt ?? "").slice(0, 200),
size: body.size || "default",
n: body.n || 1,
quality: body.quality || undefined,
};
// Build upstream request (OpenAI-compatible format)
const upstreamBody = {
model: model,
prompt: body.prompt,
};
// Pass optional parameters
if (body.n !== undefined) upstreamBody.n = body.n;
if (body.size !== undefined) upstreamBody.size = body.size;
if (body.quality !== undefined) upstreamBody.quality = body.quality;
if (body.response_format !== undefined) upstreamBody.response_format = body.response_format;
if (body.style !== undefined) upstreamBody.style = body.style;
// Build headers
const headers = {
"Content-Type": "application/json",
};
const token = credentials.apiKey || credentials.accessToken;
if (providerConfig.authHeader === "bearer") {
headers["Authorization"] = `Bearer ${token}`;
} else if (providerConfig.authHeader === "x-api-key") {
headers["x-api-key"] = token;
}
if (log) {
const promptPreview =
typeof body.prompt === "string"
? body.prompt.slice(0, 60)
: String(body.prompt ?? "").slice(0, 60);
log.info(
"IMAGE",
`${provider}/${model} | prompt: "${promptPreview}..." | size: ${body.size || "default"}`
);
}
const requestBody = JSON.stringify(upstreamBody);
// Try primary URL
let result = await fetchImageEndpoint(
providerConfig.baseUrl,
headers,
requestBody,
provider,
log
);
// Fallback for providers with fallbackUrl (e.g., Nebius)
if (
!result.success &&
providerConfig.fallbackUrl &&
[404, 410, 502, 503].includes(result.status)
) {
if (log) {
log.info("IMAGE", `${provider}: primary URL failed (${result.status}), trying fallback...`);
}
result = await fetchImageEndpoint(
providerConfig.fallbackUrl,
headers,
requestBody,
provider,
log
);
}
// Save call log after result is determined
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: result.status || (result.success ? 200 : 502),
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
tokens: { prompt_tokens: 0, completion_tokens: 0 },
error: result.success
? null
: typeof result.error === "string"
? result.error.slice(0, 500)
: null,
requestBody: logRequestBody,
responseBody: result.success ? { images_count: result.data?.data?.length || 0 } : null,
}).catch(() => {});
return result;
}
/**
* Fetch a single image endpoint and normalize response
*/
async function fetchImageEndpoint(url, headers, body, provider, log) {
try {
const response = await fetch(url, {
method: "POST",
headers,
body,
});
if (!response.ok) {
const errorText = await response.text();
if (log) {
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
}
return {
success: false,
status: response.status,
error: errorText,
};
}
const data = await response.json();
// Normalize response to OpenAI format
return {
success: true,
data: {
created: data.created || Math.floor(Date.now() / 1000),
data: data.data || [],
},
};
} catch (err) {
if (log) {
log.error("IMAGE", `${provider} fetch error: ${err.message}`);
}
return {
success: false,
status: 502,
error: `Image provider error: ${err.message}`,
};
}
}
/**
* Handle Hyperbolic image generation
* Uses { model_name, prompt, height, width } and returns { images: [{ image: base64 }] }
*/
async function handleHyperbolicImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials.apiKey || credentials.accessToken;
const [width, height] = (body.size || "1024x1024").split("x").map(Number);
const upstreamBody = {
model_name: model,
prompt: body.prompt,
height: height || 1024,
width: width || 1024,
backend: "auto",
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("IMAGE", `${provider}/${model} (hyperbolic) | prompt: "${promptPreview}..."`);
}
try {
const response = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
if (log)
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: response.status, error: errorText };
}
const data = await response.json();
// Transform { images: [{ image: base64 }] } → OpenAI format
const images = (data.images || []).map((img) => ({
b64_json: img.image,
revised_prompt: body.prompt,
}));
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { images_count: images.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: images },
};
} catch (err) {
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}
/**
* Handle NanoBanana image generation
* Uses flash vs pro routing based on model ID
*/
async function handleNanoBananaImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials.apiKey || credentials.accessToken;
// Route to pro URL for "nanobanana-pro" model
const isPro = model === "nanobanana-pro";
const url = isPro && providerConfig.proUrl ? providerConfig.proUrl : providerConfig.baseUrl;
const upstreamBody = {
prompt: body.prompt,
};
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info(
"IMAGE",
`${provider}/${model} (nanobanana ${isPro ? "pro" : "flash"}) | prompt: "${promptPreview}..."`
);
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
if (log)
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: response.status, error: errorText };
}
const data = await response.json();
// Normalize NanoBanana response to OpenAI format
const images = [];
if (data.image) {
images.push({ b64_json: data.image, revised_prompt: body.prompt });
} else if (data.images) {
for (const img of data.images) {
images.push({
b64_json: typeof img === "string" ? img : img.image || img.data,
revised_prompt: body.prompt,
});
}
} else if (data.data) {
// Already OpenAI-like
return { success: true, data };
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { images_count: images.length },
}).catch(() => {});
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: images },
};
} catch (err) {
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Image provider error: ${err.message}` };
}
}