Merge pull request #2377 from oyi77/feat/5-new-content-providers

feat(content): add Haiper, Leonardo, Ideogram, Suno, Udio providers
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-18 15:36:15 -03:00
committed by GitHub
9 changed files with 1077 additions and 2 deletions

View File

@@ -317,6 +317,40 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1:1", "16:9", "9:16", "4:3", "3:4"],
},
haiper: {
id: "haiper",
baseUrl: "https://api.haiper.ai/v1/jobs/gen2/text2image",
statusUrl: "https://api.haiper.ai/v1/jobs",
authType: "apikey",
authHeader: "HAIPER_KEY",
format: "haiper-image",
models: [{ id: "gen2", name: "Gen 2 Image" }],
supportedSizes: ["16:9", "9:16", "1:1", "4:3", "3:4"],
},
leonardo: {
id: "leonardo",
baseUrl: "https://cloud.leonardo.ai/api/rest/v1/generations",
authType: "apikey",
authHeader: "bearer",
format: "leonardo-image",
models: [
{ id: "phoenix", name: "Phoenix" },
{ id: "sdxl", name: "SDXL" },
],
supportedSizes: ["1024x1024", "1024x576", "576x1024"],
},
ideogram: {
id: "ideogram",
baseUrl: "https://api.ideogram.ai/generate",
authType: "apikey",
authHeader: "Api-Key",
format: "ideogram-image",
models: [
{ id: "V_3", name: "Ideogram V3" },
{ id: "V_2A", name: "Ideogram V2A" },
],
supportedSizes: ["1024x1024", "1024x1792", "1792x1024"],
},
sdwebui: {
id: "sdwebui",
baseUrl: "http://localhost:7860/sdapi/v1/txt2img",

View File

@@ -37,6 +37,27 @@ export const MUSIC_PROVIDERS: Record<string, MusicProvider> = {
],
},
suno: {
id: "suno",
baseUrl: "https://studio-api.suno.ai/api/generate/v2/",
statusUrl: "https://studio-api.suno.ai/api/feed/",
authType: "cookie",
authHeader: "cookie",
format: "suno-music",
models: [
{ id: "chirp-v3-5", name: "Chirp V3.5" },
{ id: "chirp-v4", name: "Chirp V4" },
],
},
udio: {
id: "udio",
baseUrl: "https://www.udio.com/api/generate-proxy",
statusUrl: "https://www.udio.com/api/songs",
authType: "cookie",
authHeader: "cookie",
format: "udio-music",
models: [{ id: "udio-default", name: "Udio Default" }],
},
minimax: {
id: "minimax",
baseUrl: "https://api.minimax.io/v1/music_generation",
@@ -50,7 +71,6 @@ export const MUSIC_PROVIDERS: Record<string, MusicProvider> = {
{ id: "music-cover", name: "Music Cover" },
],
},
comfyui: {
id: "comfyui",
baseUrl: "http://localhost:8188",

View File

@@ -1941,6 +1941,68 @@ export const REGISTRY: Record<string, RegistryEntry> = {
],
},
haiper: {
id: "haiper",
alias: "hp",
format: "openai",
executor: "default",
baseUrl: "https://api.haiper.ai/v1",
authType: "apikey",
authHeader: "HAIPER_KEY",
models: [
{ id: "gen2", name: "Gen 2 Video" },
{ id: "gen2-image", name: "Gen 2 Image" },
],
},
leonardo: {
id: "leonardo",
alias: "leo",
format: "openai",
executor: "default",
baseUrl: "https://cloud.leonardo.ai/api/rest/v1",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "phoenix", name: "Phoenix" },
{ id: "sdxl", name: "SDXL" },
],
},
ideogram: {
id: "ideogram",
alias: "ideo",
format: "openai",
executor: "default",
baseUrl: "https://api.ideogram.ai",
authType: "apikey",
authHeader: "Api-Key",
models: [
{ id: "V_3", name: "Ideogram V3" },
{ id: "V_2A", name: "Ideogram V2A" },
],
},
suno: {
id: "suno",
alias: "suno",
format: "openai",
executor: "default",
baseUrl: "https://studio-api.suno.ai/api/generate/v2/",
authType: "cookie",
authHeader: "cookie",
models: [
{ id: "chirp-v3-5", name: "Chirp V3.5" },
{ id: "chirp-v4", name: "Chirp V4" },
],
},
udio: {
id: "udio",
alias: "udio",
format: "openai",
executor: "default",
baseUrl: "https://www.udio.com/api/generate-proxy",
authType: "apikey",
authHeader: "cookie",
models: [{ id: "udio-default", name: "Udio Default" }],
},
groq: {
id: "groq",
alias: "groq",

View File

@@ -52,6 +52,24 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
],
},
haiper: {
id: "haiper",
baseUrl: "https://api.haiper.ai/v1/jobs/gen2/text2video",
statusUrl: "https://api.haiper.ai/v1/jobs",
authType: "apikey",
authHeader: "HAIPER_KEY",
format: "haiper-video",
models: [{ id: "gen2", name: "Gen 2" }],
},
leonardo: {
id: "leonardo",
baseUrl: "https://cloud.leonardo.ai/api/rest/v1/generations",
statusUrl: "https://cloud.leonardo.ai/api/rest/v1/generations",
authType: "apikey",
authHeader: "bearer",
format: "leonardo-video",
models: [{ id: "phoenix", name: "Phoenix Video" }],
},
pollinations: {
id: "pollinations",
baseUrl: "https://gen.pollinations.ai/video",
@@ -101,7 +119,6 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
{ id: "tencent/hunyuan-video", name: "Hunyuan Video" },
],
},
comfyui: {
id: "comfyui",
baseUrl: "http://localhost:8188",

View File

@@ -372,6 +372,30 @@ export async function handleImageGeneration({
});
}
if (providerConfig.format === "haiper-image") {
return handleHaiperImageGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "leonardo-image") {
return handleLeonardoImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "ideogram-image") {
return handleIdeogramImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log });
}
@@ -2976,6 +3000,328 @@ async function handleComfyUIImageGeneration({ model, provider, providerConfig, b
}
}
async function handleHaiperImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (log) {
log.info("IMAGE", `${provider}/${model} (haiper) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", HAIPER_KEY: token },
body: JSON.stringify({ prompt, aspect_ratio: body.aspect_ratio || "16:9" }),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const { job_id } = await res.json();
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await sleep(5000);
const statusRes = await fetch(`${providerConfig.statusUrl}/${job_id}`, {
headers: { HAIPER_KEY: token },
});
const status = await statusRes.json();
if (status.status === "completed" || status.status === "succeeded") {
const imgUrl = status.creation_url || status.output?.image_url;
if (imgUrl) {
const imgRes = await fetch(imgUrl);
if (!imgRes.ok) {
return {
success: false,
status: imgRes.status,
error: `Failed to download image: ${imgRes.status}`,
};
}
const buf = await imgRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
},
};
}
}
if (status.status === "failed") {
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Haiper image generation failed",
}).catch(() => {});
return { success: false, status: 502, error: "Haiper image generation failed" };
}
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Haiper image generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Haiper image generation timed out" };
} catch (err) {
if (log) log.error("IMAGE", `${provider} haiper 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}` };
}
}
async function handleLeonardoImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (log) {
log.info("IMAGE", `${provider}/${model} (leonardo) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({
modelId: model || "phoenix",
prompt,
width: body.width || 1024,
height: body.height || 1024,
num_images: 1,
}),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const { sdGenerationJob } = await res.json();
const genId = sdGenerationJob?.generationId;
if (!genId) {
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "No generation ID returned",
}).catch(() => {});
return { success: false, status: 502, error: "No generation ID returned" };
}
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await sleep(5000);
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const status = await statusRes.json();
const gen = status.generations_by_pk || status;
if (gen.status === "COMPLETE") {
const imgUrl = gen.generated_images?.[0]?.url;
if (imgUrl) {
const imgRes = await fetch(imgUrl);
if (!imgRes.ok) {
return {
success: false,
status: imgRes.status,
error: `Failed to download image: ${imgRes.status}`,
};
}
const buf = await imgRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
},
};
}
}
if (gen.status === "FAILED") {
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Leonardo image generation failed",
}).catch(() => {});
return { success: false, status: 502, error: "Leonardo image generation failed" };
}
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Leonardo image generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Leonardo image generation timed out" };
} catch (err) {
if (log) log.error("IMAGE", `${provider} leonardo 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}` };
}
}
async function handleIdeogramImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (log) {
log.info("IMAGE", `${provider}/${model} (ideogram) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Api-Key": token },
body: JSON.stringify({ prompt, aspect_ratio: "ASPECT_16_9", model: model || "V_3" }),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const data = await res.json();
if (data.data && data.data.length > 0) {
const imgUrl = data.data[0].url;
const imgRes = await fetch(imgUrl);
if (!imgRes.ok) {
return {
success: false,
status: imgRes.status,
error: `Failed to download image: ${imgRes.status}`,
};
}
const buf = await imgRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64") }],
},
};
}
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "No images returned from Ideogram",
}).catch(() => {});
return { success: false, status: 502, error: "No images returned from Ideogram" };
} catch (err) {
if (log) log.error("IMAGE", `${provider} ideogram 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}` };
}
}
type Imagen3ImageGenArgs = {
model: string;
provider: string;

View File

@@ -101,6 +101,13 @@ export async function handleMusicGeneration({ body, credentials, log }) {
return handleKieMusicGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "suno-music") {
return handleSunoMusicGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "udio-music") {
return handleUdioMusicGeneration({ model, provider, providerConfig, body, credentials, log });
}
return {
success: false,
status: 400,
@@ -366,3 +373,231 @@ async function handleKieMusicGeneration({
};
}
}
async function handleSunoMusicGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const cookie = credentials?.apiKey || credentials?.providerSpecificData?.cookie || "";
if (!cookie) {
return { success: false, status: 401, error: "Suno session cookie is required" };
}
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (log) {
log.info("MUSIC", `${provider}/${model} (suno) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({
gpt_description_prompt: prompt,
mv: model || "chirp-v3-5",
prompt: body.lyrics || "",
title: body.title || "",
tags: body.tags || "",
make_instrumental: body.instrumental || false,
}),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const clips = await res.json();
const ids = clips.map((c) => c.id).filter(Boolean);
if (ids.length === 0) {
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "No clips returned from Suno",
}).catch(() => {});
return { success: false, status: 502, error: "No clips returned from Suno" };
}
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const feedRes = await fetch(`${providerConfig.statusUrl}?ids=${ids.join(",")}`, {
headers: { Cookie: cookie },
});
const songs = await feedRes.json();
const ready = songs.filter((s) => s.audio_url);
if (ready.length > 0) {
const audioRes = await fetch(ready[0].audio_url);
if (!audioRes.ok) {
return {
success: false,
status: audioRes.status,
error: `Failed to download audio: ${audioRes.status}`,
};
}
const buf = await audioRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp3" }],
},
};
}
}
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Suno music generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Suno music generation timed out" };
} catch (err) {
if (log) log.error("MUSIC", `${provider} suno error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
}
}
async function handleUdioMusicGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const cookie = credentials?.apiKey || credentials?.providerSpecificData?.cookie || "";
if (!cookie) {
return { success: false, status: 401, error: "Udio session cookie is required" };
}
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (log) {
log.info("MUSIC", `${provider}/${model} (udio) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookie },
body: JSON.stringify({ prompt, samplerOptions: { seed: -1 } }),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const data = await res.json();
const trackIds = data.track_ids || [];
if (trackIds.length === 0) {
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "No tracks returned from Udio",
}).catch(() => {});
return { success: false, status: 502, error: "No tracks returned from Udio" };
}
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`https://www.udio.com/api/songs?songIds=${trackIds.join(",")}`,
{ headers: { Cookie: cookie } }
);
const songs = await statusRes.json();
const ready = songs.filter((s) => s.finished && s.song_path);
if (ready.length > 0) {
const audioRes = await fetch(ready[0].song_path);
if (!audioRes.ok) {
return {
success: false,
status: audioRes.status,
error: `Failed to download audio: ${audioRes.status}`,
};
}
const buf = await audioRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp3" }],
},
};
}
}
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Udio music generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Udio music generation timed out" };
} catch (err) {
if (log) log.error("MUSIC", `${provider} udio error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/music/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
}
}

View File

@@ -70,6 +70,20 @@ export async function handleVideoGeneration({ body, credentials, log }) {
return handleRunwayVideoGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "haiper-video") {
return handleHaiperVideoGeneration({ model, provider, providerConfig, body, credentials, log });
}
if (providerConfig.format === "leonardo-video") {
return handleLeonardoVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
return {
success: false,
status: 400,
@@ -759,6 +773,192 @@ async function normalizeRunwayVideoResult(task, body) {
return videos;
}
async function handleHaiperVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", HAIPER_KEY: token },
body: JSON.stringify({ prompt: body.prompt, duration: 4, aspect_ratio: "16:9" }),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const { job_id } = await res.json();
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await sleep(5000);
const statusRes = await fetch(`${providerConfig.statusUrl}/${job_id}`, {
headers: { HAIPER_KEY: token },
});
const status = await statusRes.json();
if (status.status === "completed" || status.status === "succeeded") {
const videoUrl = status.creation_url || status.output?.video_url;
if (videoUrl) {
const videoRes = await fetch(videoUrl);
const buf = await videoRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp4" }],
},
};
}
}
if (status.status === "failed") {
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Haiper video generation failed",
}).catch(() => {});
return { success: false, status: 502, error: "Haiper video generation failed" };
}
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Haiper video generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Haiper video generation timed out" };
}
async function handleLeonardoVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || "";
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({
modelId: "phoenix",
prompt: body.prompt,
width: 1024,
height: 576,
num_frames: 24,
}),
});
if (!res.ok) {
const errorText = await res.text();
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: res.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});
return { success: false, status: res.status, error: errorText };
}
const { sdGenerationJob } = await res.json();
const genId = sdGenerationJob?.generationId;
if (!genId) {
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "No generation ID returned",
}).catch(() => {});
return { success: false, status: 502, error: "No generation ID returned" };
}
const deadline = Date.now() + 300000;
while (Date.now() < deadline) {
await sleep(5000);
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const status = await statusRes.json();
const gen = status.generations_by_pk || status;
if (gen.status === "COMPLETE") {
const imgUrl = gen.generated_images?.[0]?.url;
if (imgUrl) {
const videoRes = await fetch(imgUrl);
const buf = await videoRes.arrayBuffer();
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp4" }],
},
};
}
}
if (gen.status === "FAILED") {
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Leonardo video generation failed",
}).catch(() => {});
return { success: false, status: 502, error: "Leonardo video generation failed" };
}
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 504,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: "Leonardo video generation timed out",
}).catch(() => {});
return { success: false, status: 504, error: "Leonardo video generation timed out" };
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -938,6 +938,56 @@ export const APIKEY_PROVIDERS = {
freeNote: "Free GPT-5, o-series, DeepSeek-R1, Llama 4, Grok 3 — GitHub account only.",
authHint: "Create a GitHub PAT with 'models: read' scope at github.com/settings/tokens",
},
haiper: {
id: "haiper",
alias: "hp",
name: "Haiper",
icon: "videocam",
color: "#6366F1",
textIcon: "HP",
website: "https://haiper.ai",
authHint: "Get API key at haiper.ai/haiper-api",
},
leonardo: {
id: "leonardo",
alias: "leo",
name: "Leonardo AI",
icon: "palette",
color: "#8B5CF6",
textIcon: "LE",
website: "https://leonardo.ai",
authHint: "Get API key at leonardo.ai/developer",
},
ideogram: {
id: "ideogram",
alias: "ideo",
name: "Ideogram",
icon: "image",
color: "#EC4899",
textIcon: "ID",
website: "https://ideogram.ai",
authHint: "Get API key at ideogram.ai/docs/api",
},
suno: {
id: "suno",
alias: "suno",
name: "Suno",
icon: "music_note",
color: "#F59E0B",
textIcon: "SU",
website: "https://suno.ai",
authHint: "Paste session cookie from suno.ai (Clerk auth)",
},
udio: {
id: "udio",
alias: "udio",
name: "Udio",
icon: "music_note",
color: "#10B981",
textIcon: "UD",
website: "https://udio.com",
authHint: "Paste session cookie from udio.com (Supabase auth)",
},
"cloudflare-ai": {
id: "cloudflare-ai",
alias: "cf",
@@ -1667,6 +1717,8 @@ export const VIDEO_PROVIDER_IDS = new Set([
"minimax",
"together",
"replicate",
"haiper",
"leonardo",
]);
export const EMBEDDING_RERANK_PROVIDER_IDS = new Set(["voyage-ai", "jina-ai"]);

View File

@@ -0,0 +1,109 @@
import test from "node:test";
import assert from "node:assert/strict";
// ─── Provider Registrations ─────────────────────────────────────────────────
test("haiper provider is registered", async () => {
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
assert.ok(APIKEY_PROVIDERS.haiper, "haiper should be in APIKEY_PROVIDERS");
assert.equal(APIKEY_PROVIDERS.haiper.id, "haiper");
assert.equal(APIKEY_PROVIDERS.haiper.alias, "hp");
});
test("leonardo provider is registered", async () => {
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
assert.ok(APIKEY_PROVIDERS.leonardo, "leonardo should be in APIKEY_PROVIDERS");
assert.equal(APIKEY_PROVIDERS.leonardo.id, "leonardo");
});
test("ideogram provider is registered", async () => {
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
assert.ok(APIKEY_PROVIDERS.ideogram, "ideogram should be in APIKEY_PROVIDERS");
assert.equal(APIKEY_PROVIDERS.ideogram.id, "ideogram");
});
test("suno provider is registered", async () => {
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
assert.ok(APIKEY_PROVIDERS.suno, "suno should be in APIKEY_PROVIDERS");
assert.equal(APIKEY_PROVIDERS.suno.id, "suno");
});
test("udio provider is registered", async () => {
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
assert.ok(APIKEY_PROVIDERS.udio, "udio should be in APIKEY_PROVIDERS");
assert.equal(APIKEY_PROVIDERS.udio.id, "udio");
});
// ─── VIDEO_PROVIDER_IDS ─────────────────────────────────────────────────────
test("VIDEO_PROVIDER_IDS includes haiper and leonardo", async () => {
const { VIDEO_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
assert.ok(VIDEO_PROVIDER_IDS.has("haiper"), "haiper should be in VIDEO_PROVIDER_IDS");
assert.ok(VIDEO_PROVIDER_IDS.has("leonardo"), "leonardo should be in VIDEO_PROVIDER_IDS");
});
// ─── Video Registry ─────────────────────────────────────────────────────────
test("haiper video provider is in video registry", async () => {
const { VIDEO_PROVIDERS } = await import("../../open-sse/config/videoRegistry.ts");
assert.ok(VIDEO_PROVIDERS.haiper, "haiper should be in VIDEO_PROVIDERS");
assert.equal(VIDEO_PROVIDERS.haiper.format, "haiper-video");
assert.ok(VIDEO_PROVIDERS.haiper.models.length > 0);
});
test("leonardo video provider is in video registry", async () => {
const { VIDEO_PROVIDERS } = await import("../../open-sse/config/videoRegistry.ts");
assert.ok(VIDEO_PROVIDERS.leonardo, "leonardo should be in VIDEO_PROVIDERS");
assert.equal(VIDEO_PROVIDERS.leonardo.format, "leonardo-video");
});
// ─── Image Registry ─────────────────────────────────────────────────────────
test("haiper image provider is in image registry", async () => {
const { IMAGE_PROVIDERS } = await import("../../open-sse/config/imageRegistry.ts");
assert.ok(IMAGE_PROVIDERS.haiper, "haiper should be in IMAGE_PROVIDERS");
assert.equal(IMAGE_PROVIDERS.haiper.format, "haiper-image");
});
test("leonardo image provider is in image registry", async () => {
const { IMAGE_PROVIDERS } = await import("../../open-sse/config/imageRegistry.ts");
assert.ok(IMAGE_PROVIDERS.leonardo, "leonardo should be in IMAGE_PROVIDERS");
assert.equal(IMAGE_PROVIDERS.leonardo.format, "leonardo-image");
});
test("ideogram image provider is in image registry", async () => {
const { IMAGE_PROVIDERS } = await import("../../open-sse/config/imageRegistry.ts");
assert.ok(IMAGE_PROVIDERS.ideogram, "ideogram should be in IMAGE_PROVIDERS");
assert.equal(IMAGE_PROVIDERS.ideogram.format, "ideogram-image");
});
// ─── Music Registry ─────────────────────────────────────────────────────────
test("suno music provider is in music registry", async () => {
const { MUSIC_PROVIDERS } = await import("../../open-sse/config/musicRegistry.ts");
assert.ok(MUSIC_PROVIDERS.suno, "suno should be in MUSIC_PROVIDERS");
assert.equal(MUSIC_PROVIDERS.suno.format, "suno-music");
});
test("udio music provider is in music registry", async () => {
const { MUSIC_PROVIDERS } = await import("../../open-sse/config/musicRegistry.ts");
assert.ok(MUSIC_PROVIDERS.udio, "udio should be in MUSIC_PROVIDERS");
assert.equal(MUSIC_PROVIDERS.udio.format, "udio-music");
});
// ─── Handler Functions Exist ─────────────────────────────────────────────────
test("videoGeneration handler has haiper-video dispatch", async () => {
const mod = await import("../../open-sse/handlers/videoGeneration.ts");
assert.equal(typeof mod.handleVideoGeneration, "function");
});
test("imageGeneration handler has ideogram-image dispatch", async () => {
const mod = await import("../../open-sse/handlers/imageGeneration.ts");
assert.equal(typeof mod.handleImageGeneration, "function");
});
test("musicGeneration handler has suno-music dispatch", async () => {
const mod = await import("../../open-sse/handlers/musicGeneration.ts");
assert.equal(typeof mod.handleMusicGeneration, "function");
});