diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index cb964f5745..72fd474609 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -317,6 +317,40 @@ export const IMAGE_PROVIDERS: Record = { 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", diff --git a/open-sse/config/musicRegistry.ts b/open-sse/config/musicRegistry.ts index c9a10df7ce..7ca68d5891 100644 --- a/open-sse/config/musicRegistry.ts +++ b/open-sse/config/musicRegistry.ts @@ -37,6 +37,27 @@ export const MUSIC_PROVIDERS: Record = { ], }, + 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 = { { id: "music-cover", name: "Music Cover" }, ], }, - comfyui: { id: "comfyui", baseUrl: "http://localhost:8188", diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 90490194c6..cd13770862 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1941,6 +1941,68 @@ export const REGISTRY: Record = { ], }, + 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", diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index 24ce6719b3..b876bb0b6e 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -52,6 +52,24 @@ export const VIDEO_PROVIDERS: Record = { ], }, + 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 = { { id: "tencent/hunyuan-video", name: "Hunyuan Video" }, ], }, - comfyui: { id: "comfyui", baseUrl: "http://localhost:8188", diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index b0f90c1b93..5dbb3eff02 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -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; diff --git a/open-sse/handlers/musicGeneration.ts b/open-sse/handlers/musicGeneration.ts index 14fce3df0b..6528f3ec6d 100644 --- a/open-sse/handlers/musicGeneration.ts +++ b/open-sse/handlers/musicGeneration.ts @@ -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}` }; + } +} diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index bd4fafb75f..301268ac85 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -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)); } diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 157cb32124..bafa0fe222 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -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"]); diff --git a/tests/unit/new-content-providers.test.ts b/tests/unit/new-content-providers.test.ts new file mode 100644 index 0000000000..f4adca3f29 --- /dev/null +++ b/tests/unit/new-content-providers.test.ts @@ -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"); +});