mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
109
tests/unit/new-content-providers.test.ts
Normal file
109
tests/unit/new-content-providers.test.ts
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user