diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index d95c1483c2..f6ba4ffe27 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -100,6 +100,18 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record = { format: "openai", models: [{ id: "qwen3-asr", name: "Qwen3 ASR" }], }, + + kie: { + id: "kie", + baseUrl: "https://api.kie.ai", + authType: "apikey", + authHeader: "bearer", + format: "kie-audio", + models: [ + { id: "elevenlabs/speech-to-text", name: "ElevenLabs STT" }, + { id: "elevenlabs/audio-isolation", name: "ElevenLabs Audio Isolation" }, + ], + }, }; export const AUDIO_SPEECH_PROVIDERS: Record = { @@ -246,6 +258,20 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { id: "Play3.0-mini", name: "Play3.0 Mini" }, ], }, + + kie: { + id: "kie", + baseUrl: "https://api.kie.ai", + authType: "apikey", + authHeader: "bearer", + format: "kie-audio", + models: [ + { id: "elevenlabs/text-to-speech-multilingual-v2", name: "ElevenLabs TTS v2" }, + { id: "elevenlabs/text-to-speech-turbo-2-5", name: "ElevenLabs TTS Turbo 2.5" }, + { id: "elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Text to Dialogue v3" }, + { id: "elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effect v2" }, + ], + }, }; /** diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 2315608f02..17f9162caa 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -10,6 +10,7 @@ interface ImageModelEntry { name: string; inputModalities?: string[]; description?: string; + isMarket?: boolean; } interface ImageProviderConfig { @@ -233,12 +234,49 @@ export const IMAGE_PROVIDERS: Record = { kie: { id: "kie", - baseUrl: "https://api.kie.ai/api/v1/gpt4o-image/generate", - statusUrl: "https://api.kie.ai/api/v1/gpt4o-image/record-info", + baseUrl: "https://api.kie.ai", + statusUrl: "https://api.kie.ai/api/v1/jobs/recordInfo", authType: "apikey", authHeader: "bearer", format: "kie-image", - models: [{ id: "gpt4o-image", name: "KIE 4o Image" }], + models: [ + { id: "gpt4o-image", name: "KIE 4o Image" }, + { id: "seedream/4.5-text-to-image", name: "Seedream 4.5", isMarket: true }, + { id: "seedream/4.5-edit", name: "Seedream 4.5 Edit", isMarket: true }, + { id: "seedream/5.0-lite-text-to-image", name: "Seedream 5.0 Lite", isMarket: true }, + { id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true }, + { id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true }, + { id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true }, + { id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true }, + { id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true }, + { id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true }, + { id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true }, + { id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true }, + { id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true }, + { id: "google-imagen/nano-banana-edit", name: "Nano Banana Edit", isMarket: true }, + { id: "flux/2-pro-image-to-image", name: "Flux 2 Pro I2I", isMarket: true }, + { id: "flux/2-pro-text-to-image", name: "Flux 2 Pro T2I", isMarket: true }, + { id: "flux/2-image-to-image", name: "Flux 2 I2I", isMarket: true }, + { id: "flux/2-text-to-image", name: "Flux 2 T2I", isMarket: true }, + { id: "flux/kontext", name: "Flux Kontext", isMarket: true }, + { id: "grok-imagine/text-to-image", name: "Grok Imagine T2I", isMarket: true }, + { id: "grok-imagine/image-to-image", name: "Grok Imagine I2I", isMarket: true }, + { id: "gpt/gpt-image-1.5-text-to-image", name: "GPT Image 1.5 T2I", isMarket: true }, + { id: "gpt/gpt-image-1.5-image-to-image", name: "GPT Image 1.5 I2I", isMarket: true }, + { id: "gpt/gpt-image-2-text-to-image", name: "GPT Image 2 T2I", isMarket: true }, + { id: "gpt/gpt-image-2-image-to-image", name: "GPT Image 2 I2I", isMarket: true }, + { id: "ideogram/v3-text-to-image", name: "Ideogram v3", isMarket: true }, + { id: "ideogram/v3-edit", name: "Ideogram v3 Edit", isMarket: true }, + { id: "ideogram/v3-remix", name: "Ideogram v3 Remix", isMarket: true }, + { id: "ideogram/v3-reframe", name: "Ideogram v3 Reframe", isMarket: true }, + { id: "qwen/text-to-image", name: "Qwen T2I", isMarket: true }, + { id: "qwen/image-to-image", name: "Qwen I2I", isMarket: true }, + { id: "qwen/image-edit", name: "Qwen Edit", isMarket: true }, + { id: "qwen2/image-edit", name: "Qwen2 Edit", isMarket: true }, + { id: "qwen2/text-to-image", name: "Qwen2 T2I", isMarket: true }, + { id: "wan/2.7-image", name: "Wan 2.7 Image", isMarket: true }, + { id: "wan/2.7-image-pro", name: "Wan 2.7 Image Pro", isMarket: true }, + ], supportedSizes: ["1:1", "16:9", "9:16", "4:3", "3:4"], }, diff --git a/open-sse/config/musicRegistry.ts b/open-sse/config/musicRegistry.ts index 6cce3b22ee..38ae3c779c 100644 --- a/open-sse/config/musicRegistry.ts +++ b/open-sse/config/musicRegistry.ts @@ -10,6 +10,7 @@ import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtil interface MusicModel { id: string; name: string; + isMarket?: boolean; } interface MusicProvider { @@ -26,14 +27,13 @@ export const MUSIC_PROVIDERS: Record = { kie: { id: "kie", baseUrl: "https://api.kie.ai", - statusUrl: "https://api.kie.ai/api/v1/generate/record-info", + statusUrl: "https://api.kie.ai/api/v1/jobs/recordInfo", authType: "apikey", authHeader: "bearer", format: "kie-music", models: [ - { id: "V4", name: "Suno V4" }, - { id: "V4_5", name: "Suno V4.5" }, - { id: "V5", name: "Suno V5" }, + { id: "suno-v3.5", name: "Suno V3.5" }, + { id: "suno-v4.0", name: "Suno V4.0" }, ], }, diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index 2778c3e0b8..1d1fc952dd 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -10,6 +10,7 @@ import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtil interface VideoModel { id: string; name: string; + isMarket?: boolean; } interface VideoProvider { @@ -31,23 +32,33 @@ export const VIDEO_PROVIDERS: Record = { authHeader: "bearer", format: "kie-video", models: [ - { id: "kling-2.6/text-to-video", name: "Kling 2.6 Text to Video" }, - { id: "kling/v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, - { id: "kling/v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, - { id: "kling/v25-turbo-image-to-video-pro", name: "Kling v2.5 Turbo I2V Pro" }, - { id: "kling/v25-turbo-text-to-video-pro", name: "Kling v2.5 Turbo T2V Pro" }, - { id: "wan/2-6-text-to-video", name: "Wan 2.6 Text to Video" }, - { id: "wan/2-6-image-to-video", name: "Wan 2.6 Image to Video" }, - { id: "wan/2-7-text-to-video", name: "Wan 2.7 Text to Video" }, - { id: "wan/2-7-image-to-video", name: "Wan 2.7 Image to Video" }, - { id: "sora2/sora-2-text-to-video", name: "Sora 2 Text to Video" }, - { id: "sora2/sora-2-image-to-video", name: "Sora 2 Image to Video" }, - { id: "hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro" }, - { id: "hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro" }, - { id: "grok-imagine/text-to-video", name: "Grok Imagine T2V" }, - { id: "grok-imagine/image-to-video", name: "Grok Imagine I2V" }, - { id: "bytedance/v1-pro-text-to-video", name: "Bytedance v1 Pro T2V" }, - { id: "bytedance/v1-pro-image-to-video", name: "Bytedance v1 Pro I2V" }, + { id: "veo/veo-3-1", name: "Veo 3.1", isMarket: true }, + { id: "veo/veo-3-1-fast", name: "Veo 3.1 Fast", isMarket: true }, + { + id: "kling/kling-v2-1-master-text-to-video", + name: "Kling v2.1 Master T2V", + isMarket: true, + }, + { + id: "kling/kling-v2-1-master-image-to-video", + name: "Kling v2.1 Master I2V", + isMarket: true, + }, + { id: "kling/v2-5-turbo-text-to-video", name: "Kling v2.5 Turbo T2V", isMarket: true }, + { id: "kling/v2-5-turbo-image-to-video", name: "Kling v2.5 Turbo I2V", isMarket: true }, + { id: "kling/v3-0", name: "Kling v3.0", isMarket: true }, + { id: "wan/2-6-text-to-video", name: "Wan 2.6 T2V", isMarket: true }, + { id: "wan/2-6-image-to-video", name: "Wan 2.6 I2V", isMarket: true }, + { id: "wan/2-7-text-to-video", name: "Wan 2.7 T2V", isMarket: true }, + { id: "wan/2-7-image-to-video", name: "Wan 2.7 I2V", isMarket: true }, + { id: "sora2/sora-2-text-to-video", name: "Sora 2 T2V", isMarket: true }, + { id: "sora2/sora-2-image-to-video", name: "Sora 2 I2V", isMarket: true }, + { id: "hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro", isMarket: true }, + { id: "hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro", isMarket: true }, + { id: "grok-imagine/text-to-video", name: "Grok Imagine T2V", isMarket: true }, + { id: "grok-imagine/image-to-video", name: "Grok Imagine I2V", isMarket: true }, + { id: "bytedance/v2-0-text-to-video", name: "Seedance v2.0 T2V", isMarket: true }, + { id: "bytedance/v2-0-fast-text-to-video", name: "Seedance v2.0 Fast T2V", isMarket: true }, ], }, diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index d6386eb066..2153aa5b51 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -14,6 +14,7 @@ import { VertexExecutor } from "./vertex.ts"; import { CliproxyapiExecutor } from "./cliproxyapi.ts"; import { PerplexityWebExecutor } from "./perplexity-web.ts"; import { GrokWebExecutor } from "./grok-web.ts"; +import { KieExecutor } from "./kie.ts"; const executors = { antigravity: new AntigravityExecutor(), @@ -38,6 +39,7 @@ const executors = { "perplexity-web": new PerplexityWebExecutor(), "pplx-web": new PerplexityWebExecutor(), // Alias "grok-web": new GrokWebExecutor(), + kie: new KieExecutor(), }; const defaultCache = new Map(); @@ -69,3 +71,4 @@ export { CliproxyapiExecutor } from "./cliproxyapi.ts"; export { VertexExecutor } from "./vertex.ts"; export { PerplexityWebExecutor } from "./perplexity-web.ts"; export { GrokWebExecutor } from "./grok-web.ts"; +export { KieExecutor } from "./kie.ts"; diff --git a/open-sse/executors/kie.ts b/open-sse/executors/kie.ts new file mode 100644 index 0000000000..fbb2758822 --- /dev/null +++ b/open-sse/executors/kie.ts @@ -0,0 +1,140 @@ +import { BaseExecutor } from "./base.ts"; +import { sleep } from "../utils/sleep.ts"; + +type KieTaskInput = { + baseUrl: string; + token: string; + payload: unknown; + endpoint?: string; +}; + +type KiePollInput = { + statusUrl: string; + taskId: string; + token: string; + timeoutMs: number; + pollIntervalMs: number; +}; + +export type KieTaskState = "success" | "failed" | "pending"; + +export type KieTaskRecord = { + data: any; + state: KieTaskState; +}; + +function normalizeBaseUrl(baseUrl: string): string { + return baseUrl.replace(/\/$/, ""); +} + +export function normalizeKieTaskState(recordData: any): KieTaskState { + const state = String( + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" + ).toUpperCase(); + + if ( + state === "SUCCESS" || + state === "1" || + state === "FINISHED" || + state === "COMPLETE" || + state === "COMPLETED" || + state === "FIRST_SUCCESS" || + state === "ALL_SUCCESS" || + state.includes("SUCCESS") + ) { + return "success"; + } + + if ( + state === "FAIL" || + state === "FAILED" || + state === "ERROR" || + state === "2" || + state === "3" || + state.includes("FAIL") || + state.includes("ERROR") || + state === "CREATE_TASK_FAILED" || + state === "GENERATE_FAILED" + ) { + return "failed"; + } + + return "pending"; +} + +export class KieExecutor extends BaseExecutor { + constructor() { + super("kie", { baseUrl: "https://api.kie.ai" }); + } + + getTaskCreateUrl(baseUrl: string, endpoint = "/api/v1/jobs/createTask"): string { + return `${normalizeBaseUrl(baseUrl)}${endpoint}`; + } + + getTaskStatusUrl(baseUrl: string): string { + return `${normalizeBaseUrl(baseUrl)}/api/v1/jobs/recordInfo`; + } + + async createTask({ baseUrl, token, payload, endpoint }: KieTaskInput): Promise { + const res = await fetch(this.getTaskCreateUrl(baseUrl, endpoint), { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const error = await res.text(); + throw Object.assign(new Error(error || `Kie createTask failed with status ${res.status}`), { + status: res.status, + }); + } + + return res.json(); + } + + async pollTask({ + statusUrl, + taskId, + token, + timeoutMs, + pollIntervalMs, + }: KiePollInput): Promise { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + const pollUrl = new URL(statusUrl); + pollUrl.searchParams.set("taskId", String(taskId)); + + const res = await fetch(pollUrl.toString(), { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + const error = await res.text(); + throw Object.assign(new Error(error || `Kie poll failed with status ${res.status}`), { + status: res.status, + }); + } + + const data = await res.json(); + const state = normalizeKieTaskState(data); + if (state !== "pending") { + return { data, state }; + } + + await sleep(pollIntervalMs); + } + + throw Object.assign(new Error("Kie task timed out"), { status: 504 }); + } +} + +export const kieExecutor = new KieExecutor(); diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index 424ef10774..27d0853d6c 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -19,6 +19,7 @@ import { getCorsOrigin } from "../utils/cors.ts"; import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts"; import { buildAuthHeaders } from "../config/registryUtils.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { errorResponse } from "../utils/error.ts"; /** @@ -68,6 +69,104 @@ function audioStreamResponse(res, defaultContentType = "audio/mpeg") { }); } +function getKieCallbackUrl(body: any): string { + return ( + body.callBackUrl || + body.callback_url || + body.callbackUrl || + "https://omniroute.local/api/kie/callback" + ); +} + +function normalizeKieElevenLabsVoice(voice: unknown): string { + const value = typeof voice === "string" ? voice.trim() : ""; + const aliases: Record = { + alloy: "Rachel", + echo: "Adam", + fable: "Brian", + onyx: "Antoni", + nova: "Bella", + shimmer: "Dorothy", + }; + return aliases[value.toLowerCase()] || value || "Rachel"; +} + +function parseKieResultJson(recordData: any): any { + try { + return typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + return {}; + } +} + +function findAudioUrlDeep(value: any): string | null { + if (!value) return null; + + if (typeof value === "string") { + if (/^https?:\/\//i.test(value) && !/\.(jpg|jpeg|png|webp|gif|svg)(\?|$)/i.test(value)) { + return value; + } + return null; + } + + if (Array.isArray(value)) { + for (const item of value) { + const url = findAudioUrlDeep(item); + if (url) return url; + } + return null; + } + + if (typeof value === "object") { + const preferredKeys = [ + "audio_url", + "audioUrl", + "stream_audio_url", + "streamAudioUrl", + "resultUrl", + "url", + "downloadUrl", + "resultUrls", + ]; + + for (const key of preferredKeys) { + const url = findAudioUrlDeep(value[key]); + if (url) return url; + } + + for (const item of Object.values(value)) { + const url = findAudioUrlDeep(item); + if (url) return url; + } + } + + return null; +} + +function findKieAudioUrl(recordData: any): string | null { + const resultJson = parseKieResultJson(recordData); + const candidates = [ + recordData?.data?.response, + recordData?.data, + resultJson, + ...(Array.isArray(recordData?.data?.response) ? recordData.data.response : []), + ...(Array.isArray(recordData?.data?.data) ? recordData.data.data : []), + ...(Array.isArray(resultJson?.data) ? resultJson.data : []), + ...(Array.isArray(resultJson?.result) ? resultJson.result : []), + ]; + + for (const item of candidates) { + const url = findAudioUrlDeep(item); + if (url) { + return url; + } + } + + return null; +} + /** * Validate a path segment to prevent path traversal / SSRF. * Returns true if safe, false if it contains traversal sequences. @@ -321,6 +420,100 @@ async function handlePlayHtSpeech(providerConfig, body, modelId, token) { return audioStreamResponse(res); } +/** + * Handle Kie.ai TTS + * Kie.ai has model-specific endpoints or uses unified jobs API. + */ +async function handleKieAudioSpeech(providerConfig, body, modelId, token) { + const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + + const voice = normalizeKieElevenLabsVoice(body.voice); + + const payload = { + model: modelId, + callBackUrl: getKieCallbackUrl(body), + input: { + text: body.input, + voice, + stability: typeof body.stability === "number" ? body.stability : 0.5, + similarity_boost: typeof body.similarity_boost === "number" ? body.similarity_boost : 0.75, + style: typeof body.style === "number" ? body.style : 0, + speed: typeof body.speed === "number" ? body.speed : 1, + timestamps: body.timestamps === true, + previous_text: body.previous_text || "", + next_text: body.next_text || "", + language_code: body.language_code || "", + }, + }; + + let data; + try { + data = await kieExecutor.createTask({ + baseUrl, + token, + payload, + }); + } catch (err: any) { + return Response.json( + { + error: { message: err?.message || "Kie audio createTask failed", code: err?.status || 502 }, + }, + { + status: Number(err?.status) || 502, + headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + } + ); + } + const taskId = data?.data?.taskId || data?.taskId; + + if (taskId) { + return pollKieAudioResult(baseUrl, modelId, taskId, token); + } + + const audioUrl = findKieAudioUrl(data); + if (typeof audioUrl === "string" && audioUrl.length > 0) { + const audioRes = await fetch(audioUrl); + return audioStreamResponse(audioRes); + } + + return errorResponse( + 502, + data?.msg || data?.message || "Kie audio generation did not return taskId or audio URL" + ); +} + +/** + * Internal polling for Kie.ai async audio tasks + */ +async function pollKieAudioResult(baseUrl, modelId, taskId, token) { + const statusUrl = kieExecutor.getTaskStatusUrl(baseUrl); + try { + const { data, state } = await kieExecutor.pollTask({ + statusUrl, + taskId: String(taskId), + token, + timeoutMs: 60000, + pollIntervalMs: 2000, + }); + + if (state === "success") { + const url = findKieAudioUrl(data); + if (url) { + const audioRes = await fetch(url); + return audioStreamResponse(audioRes); + } + return errorResponse(502, "Kie audio task completed without audio URL"); + } + } catch (err: any) { + return errorResponse( + Number(err?.status) || 504, + err?.message || "Kie audio generation timed out or failed" + ); + } + + return errorResponse(504, "Kie audio generation timed out or failed"); +} + /** * Handle Coqui TTS (local, no auth) * POST {baseUrl} with { text, speaker_id } → WAV audio @@ -457,6 +650,10 @@ export async function handleAudioSpeech({ return handlePlayHtSpeech(providerConfig, body, modelId, token); } + if (providerConfig.format === "kie-audio") { + return handleKieAudioSpeech(providerConfig, body, modelId, token); + } + if (providerConfig.format === "coqui") { return handleCoquiSpeech(providerConfig, body); } diff --git a/open-sse/handlers/audioTranscription.ts b/open-sse/handlers/audioTranscription.ts index 0ece148e23..9a6a9b2e87 100644 --- a/open-sse/handlers/audioTranscription.ts +++ b/open-sse/handlers/audioTranscription.ts @@ -1,4 +1,5 @@ import { getCorsOrigin } from "../utils/cors.ts"; +import { Buffer } from "node:buffer"; /** * Audio Transcription Handler * @@ -19,6 +20,7 @@ import { type AudioProvider, } from "../config/audioRegistry.ts"; import { buildAuthHeaders } from "../config/registryUtils.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { errorResponse } from "../utils/error.ts"; type TranscriptionCredentials = { @@ -284,6 +286,89 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }); } +/** + * Handle Kie.ai transcription + */ +async function handleKieAudioTranscription(providerConfig, file, modelId, token) { + const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + const fileBuffer = await file.arrayBuffer(); + const fileBase64 = Buffer.from(fileBuffer).toString("base64"); + let data; + try { + data = await kieExecutor.createTask({ + baseUrl, + token, + payload: { + model: modelId, + input: { + file_name: getUploadedFileName(file), + file_base64: fileBase64, + }, + }, + }); + } catch (err: any) { + return Response.json( + { + error: { + message: err?.message || "Kie transcription createTask failed", + code: err?.status || 502, + }, + }, + { + status: Number(err?.status) || 502, + headers: { "Access-Control-Allow-Origin": getCorsOrigin() }, + } + ); + } + const taskId = data?.data?.taskId || data?.taskId; + + if (taskId) { + return pollKieTranscriptionResult(baseUrl, modelId, taskId, token); + } + + return Response.json( + { text: data?.data?.text || data?.text || "" }, + { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } + ); +} + +/** + * Internal polling for Kie.ai async transcription tasks + */ +async function pollKieTranscriptionResult(baseUrl, modelId, taskId, token) { + void modelId; + const statusUrl = kieExecutor.getTaskStatusUrl(baseUrl); + try { + const { data, state } = await kieExecutor.pollTask({ + statusUrl, + taskId: String(taskId), + token, + timeoutMs: 120000, + pollIntervalMs: 2000, + }); + + if (state === "success") { + const text = + data?.data?.response?.text || + data?.data?.resultText || + data?.data?.text || + data?.text || + ""; + return Response.json( + { text }, + { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } } + ); + } + } catch (err: any) { + return errorResponse( + Number(err?.status) || 504, + err?.message || "Kie transcription generation timed out or failed" + ); + } + + return errorResponse(504, "Kie transcription generation timed out or failed"); +} + /** * Handle audio transcription request * @@ -354,6 +439,10 @@ export async function handleAudioTranscription({ return handleHuggingFaceTranscription(providerConfig, file, modelId, token); } + if (providerConfig.format === "kie-audio") { + return handleKieAudioTranscription(providerConfig, file, modelId, token); + } + // Default: OpenAI/Groq/Qwen3-compatible multipart proxy const upstreamForm = new FormData(); upstreamForm.append("file", file, getUploadedFileName(file)); diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index fb14b6540c..03a8e45ecd 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -17,6 +17,7 @@ import { randomUUID } from "crypto"; */ import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { mapImageSize } from "../translator/image/sizeMapper.ts"; import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../utils/sleep.ts"; @@ -299,6 +300,46 @@ export async function handleImageGeneration({ body, credentials, log, resolvedPr return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log }); } +function normalizeKieImageResult(recordData: any): string[] { + let resultJson: Record = {}; + try { + resultJson = + typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + resultJson = {}; + } + + const urls = new Set(); + + const add = (val: any) => { + if (typeof val === "string" && val.startsWith("http")) urls.add(val); + if (Array.isArray(val)) { + val.forEach((v) => { + if (typeof v === "string" && v.startsWith("http")) urls.add(v); + }); + } + }; + + // Check resultJson (common in Market API) + add(resultJson?.resultUrls); + add(resultJson?.imageUrls); + add(resultJson?.resultUrl); + add(resultJson?.imageUrl); + + // Check data.response (common in 4o-image API) + add(recordData?.data?.response?.resultUrls); + add(recordData?.data?.response?.resultUrl); + + // Check direct data fields + add(recordData?.data?.resultImageUrls); + add(recordData?.data?.resultImageUrl); + add(recordData?.data?.url); + + return Array.from(urls); +} + async function handleKieImageGeneration({ model, provider, @@ -312,58 +353,88 @@ async function handleKieImageGeneration({ const timeoutMs = normalizePositiveNumber(body.timeout_ms, 300000); const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, 2500); - const payload = { - prompt: body.prompt, - image_size: mapImageSize(body.size, "1:1"), - num_images: body.n || 1, - }; + // Check if model is a Market model (unified API) + const fullRegistry = getImageProvider(provider); + const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model); + const isMarket = modelEntry?.isMarket || model.includes("/"); + + const { imageUrl } = extractImageInputs(body); + let baseUrl = ""; + let payload: any = {}; + + if (isMarket) { + // Unified Market API endpoint + baseUrl = `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/createTask`; + // Strip category prefix (e.g., "gpt/gpt-image-2" -> "gpt-image-2") + const marketModelId = model.includes("/") ? model.split("/").pop() : model; + payload = { + model: marketModelId, + input: { + prompt: body.prompt, + aspect_ratio: mapImageSize(body.size, "1:1"), + }, + }; + if (imageUrl) { + payload.input.image_url = imageUrl; + } + } else { + // Legacy/Direct endpoint + const modelPath = model.replace("-t2i", "").replace("-i2i", ""); + baseUrl = providerConfig.baseUrl.includes(model) + ? providerConfig.baseUrl + : `https://api.kie.ai/api/v1/${modelPath}/generate`; + + payload = { + prompt: body.prompt, + image_size: mapImageSize(body.size, "1:1"), + num_images: body.n || 1, + }; + } if (log) { const promptPreview = String(body.prompt ?? "").slice(0, 60); - log.info("IMAGE", `${provider}/${model} (kie-image) | prompt: "${promptPreview}..."`); + log.info( + "IMAGE", + `${provider}/${model} (${isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."` + ); } try { - const createRes = await fetch(providerConfig.baseUrl, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), + const endpoint = isMarket ? "/api/v1/jobs/createTask" : new URL(baseUrl).pathname; + const createBaseUrl = isMarket ? providerConfig.baseUrl : baseUrl.replace(endpoint, ""); + const createData = await kieExecutor.createTask({ + baseUrl: createBaseUrl, + token, + payload, + endpoint, }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return saveImageErrorResult({ - provider, - model, - status: createRes.status, - startTime, - error: errorText, - requestBody: payload, - }); - } - - const createData = await createRes.json(); const taskId = createData?.data?.taskId || createData?.taskId; + if (!taskId) { + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE image generation did not return taskId"; + if (log) { + log.error("IMAGE", `KIE createTask failed: ${JSON.stringify(createData)}`); + } return saveImageErrorResult({ provider, model, status: 502, startTime, - error: "KIE image generation did not return taskId", + error: errorMessage, requestBody: payload, }); } // Use statusUrl from providerConfig if available, fallback to dynamic derivation - const statusUrl = - providerConfig.statusUrl || - providerConfig.baseUrl - .replace(/\/generate$/, "/record-info") - .replace("/api/v1/gpt4o-image/generate", "/api/v1/gpt4o-image/record-info"); + const statusUrl = isMarket + ? `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/recordInfo` + : providerConfig.statusUrl && !providerConfig.statusUrl.includes("jobs/recordInfo") + ? providerConfig.statusUrl + : baseUrl.replace(/\/generate$/, "/record-info"); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { @@ -391,18 +462,20 @@ async function handleKieImageGeneration({ const recordData = await recordRes.json(); const state = String( - recordData?.data?.status ?? recordData?.data?.successFlag ?? recordData?.msg ?? "PENDING" + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" ).toUpperCase(); if (state === "SUCCESS" || state === "1" || state === "FINISHED") { - const urls = Array.isArray(recordData?.data?.response?.resultUrls) - ? recordData.data.response.resultUrls - : Array.isArray(recordData?.data?.resultImageUrls) - ? recordData.data.resultImageUrls - : []; - const images = urls - .filter((url: unknown) => typeof url === "string" && url.length > 0) - .map((url: unknown) => ({ url: url as string, revised_prompt: body.prompt })); + if (log) { + log.info("IMAGE", `KIE poll success for task ${taskId}`); + } + const urls = normalizeKieImageResult(recordData); + const images = urls.map((url: string) => ({ url, revised_prompt: body.prompt })); + return saveImageSuccessResult({ provider, model, @@ -430,6 +503,11 @@ async function handleKieImageGeneration({ recordData?.data?.failMsg || recordData?.msg || `KIE image task failed with status: ${state}`; + + if (log) { + log.error("IMAGE", `KIE poll failed for task ${taskId}: ${JSON.stringify(recordData)}`); + } + return saveImageErrorResult({ provider, model, diff --git a/open-sse/handlers/musicGeneration.ts b/open-sse/handlers/musicGeneration.ts index a8092d88c3..0dc197231d 100644 --- a/open-sse/handlers/musicGeneration.ts +++ b/open-sse/handlers/musicGeneration.ts @@ -15,6 +15,7 @@ */ import { getMusicProvider, parseMusicModel } from "../config/musicRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { submitComfyWorkflow, pollComfyResult, @@ -24,6 +25,63 @@ import { import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../utils/sleep.ts"; +function getKieCallbackUrl(body: any): string { + return ( + body.callBackUrl || + body.callback_url || + body.callbackUrl || + "https://omniroute.local/api/kie/callback" + ); +} + +function normalizeKieSunoModel(model: string): string { + const map: Record = { + "suno-v3.5": "V3_5", + "suno-v4.0": "V4", + }; + return map[model] || model; +} + +function parseKieResultJson(recordData: any): any { + try { + return typeof recordData?.data?.resultJson === "string" + ? JSON.parse(recordData.data.resultJson) + : recordData?.data?.resultJson || {}; + } catch { + return {}; + } +} + +function normalizeKieMusicTracks(recordData: any): any[] { + const resultJson = parseKieResultJson(recordData); + const candidates = [ + recordData?.data?.response?.sunoData, + recordData?.data?.response?.data, + recordData?.data?.data, + recordData?.data?.sunoData, + resultJson?.sunoData, + resultJson?.data, + resultJson?.result, + ]; + + for (const candidate of candidates) { + if (Array.isArray(candidate) && candidate.length > 0) { + return candidate; + } + } + + const singleUrl = + recordData?.data?.response?.audioUrl || + recordData?.data?.response?.audio_url || + recordData?.data?.resultUrl || + recordData?.data?.audio_url || + resultJson?.audioUrl || + resultJson?.audio_url || + resultJson?.url; + + return typeof singleUrl === "string" && singleUrl.length > 0 ? [{ audioUrl: singleUrl }] : []; +} + /** * Handle music generation request */ @@ -190,41 +248,64 @@ async function handleKieMusicGeneration({ const pollIntervalMs = Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500; const token = credentials?.apiKey || credentials?.accessToken; const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); - const payload = { - prompt: body.prompt, - customMode: false, - instrumental: true, - model, - }; + + // Check if model is a Market model + const fullRegistry = getMusicProvider(provider); + const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model); + const isMarket = modelEntry?.isMarket || model.includes("/"); + + let url = ""; + let payload: any = {}; + + if (isMarket) { + url = `${baseUrl}/api/v1/jobs/createTask`; + payload = { + model: model.includes("/") ? model.split("/").pop() : model, + callBackUrl: getKieCallbackUrl(body), + input: { + prompt: body.prompt, + instrumental: true, + }, + }; + } else { + url = `${baseUrl}/api/v1/generate`; + payload = { + prompt: body.prompt, + customMode: false, + instrumental: true, + model: normalizeKieSunoModel(model), + callBackUrl: getKieCallbackUrl(body), + }; + } if (log) { const promptPreview = String(body.prompt ?? "").slice(0, 60); - log.info("MUSIC", `${provider}/${model} (kie-music) | prompt: "${promptPreview}..."`); + log.info( + "MUSIC", + `${provider}/${model} (${isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."` + ); } try { - const createRes = await fetch(`${baseUrl}/api/v1/generate`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return { success: false, status: createRes.status, error: errorText }; - } - - const createData = await createRes.json(); + const endpoint = new URL(url).pathname; + const createData = await kieExecutor.createTask({ baseUrl, token, payload, endpoint }); const taskId = createData?.data?.taskId || createData?.taskId; if (!taskId) { - return { success: false, status: 502, error: "KIE music generation did not return taskId" }; + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE music generation did not return taskId"; + if (log) { + log.error("MUSIC", `KIE createTask failed: ${JSON.stringify(createData)}`); + } + return { success: false, status: 502, error: errorMessage }; } const deadline = Date.now() + timeoutMs; - const statusUrl = providerConfig.statusUrl || `${baseUrl}/api/v1/generate/record-info`; + const statusUrl = isMarket + ? `${baseUrl}/api/v1/jobs/recordInfo` + : providerConfig.statusUrl || `${baseUrl}/api/v1/generate/record-info`; while (Date.now() < deadline) { const pollUrl = new URL(statusUrl); @@ -243,16 +324,22 @@ async function handleKieMusicGeneration({ } const recordData = await recordRes.json(); - const state = String(recordData?.data?.status || recordData?.msg || "PENDING").toUpperCase(); + const state = String( + recordData?.data?.status ?? + recordData?.data?.state ?? + recordData?.data?.successFlag ?? + recordData?.msg ?? + "PENDING" + ).toUpperCase(); if (state === "SUCCESS" || state === "1" || state === "FINISHED") { - const tracks = Array.isArray(recordData?.data?.response?.sunoData) - ? recordData.data.response.sunoData - : []; + const tracks = normalizeKieMusicTracks(recordData); + const audioFiles = tracks - .map((track: unknown) => { - const t = track as Record; - return (typeof t?.audioUrl === "string" ? t.audioUrl : t?.url) as string; + .map((track: any) => { + return ( + typeof track?.audioUrl === "string" ? track.audioUrl : track?.audio_url || track?.url + ) as string; }) .filter((url: string) => typeof url === "string" && url.length > 0) .map((url: string) => ({ url, format: "mp3" })); diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 98b05e9208..6ee997f275 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -16,6 +16,7 @@ */ import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts"; +import { kieExecutor } from "../executors/kie.ts"; import { submitComfyWorkflow, pollComfyResult, @@ -311,8 +312,11 @@ async function handleKieVideoGeneration({ const token = credentials?.apiKey || credentials?.accessToken; const baseUrl = providerConfig.baseUrl.replace(/\/$/, ""); + // Strip category prefix (e.g., "veo/veo-3-1" -> "veo-3-1") + const marketModelId = model.includes("/") ? model.split("/").pop() : model; + const payload = { - model, + model: marketModelId, input: { prompt: body.prompt, duration: body.duration ? String(body.duration) : "5", @@ -327,24 +331,18 @@ async function handleKieVideoGeneration({ } try { - const createRes = await fetch(`${baseUrl}/api/v1/jobs/createTask`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!createRes.ok) { - const errorText = await createRes.text(); - return { success: false, status: createRes.status, error: errorText }; - } - - const createData = await createRes.json(); + const createData = await kieExecutor.createTask({ baseUrl, token, payload }); const taskId = createData?.data?.taskId || createData?.taskId; if (!taskId) { - return { success: false, status: 502, error: "KIE video generation did not return taskId" }; + const errorMessage = + createData?.msg || + createData?.message || + createData?.error || + "KIE video generation did not return taskId"; + if (log) { + log.error("VIDEO", `KIE createTask failed: ${JSON.stringify(createData)}`); + } + return { success: false, status: 502, error: errorMessage }; } const deadline = Date.now() + timeoutMs; diff --git a/public/providers/kie.png b/public/providers/kie.png new file mode 100644 index 0000000000..439f8fbc65 Binary files /dev/null and b/public/providers/kie.png differ diff --git a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx index 0a5865479e..8fea6babe8 100644 --- a/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { IMAGE_PROVIDERS } from "@omniroute/open-sse/config/imageRegistry.ts"; @@ -93,23 +93,18 @@ const PROVIDER_MODELS: Record< id: "kie", name: "KIE.AI", models: [ - { id: "kie/kling-2.6/text-to-video", name: "Kling 2.6 Text to Video" }, - { id: "kie/kling/v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, - { id: "kie/kling/v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, - { id: "kie/kling/v25-turbo-image-to-video-pro", name: "Kling v2.5 Turbo I2V Pro" }, - { id: "kie/kling/v25-turbo-text-to-video-pro", name: "Kling v2.5 Turbo T2V Pro" }, - { id: "kie/wan/2-6-text-to-video", name: "Wan 2.6 Text to Video" }, - { id: "kie/wan/2-6-image-to-video", name: "Wan 2.6 Image to Video" }, - { id: "kie/wan/2-7-text-to-video", name: "Wan 2.7 Text to Video" }, - { id: "kie/wan/2-7-image-to-video", name: "Wan 2.7 Image to Video" }, - { id: "kie/sora2/sora-2-text-to-video", name: "Sora 2 Text to Video" }, - { id: "kie/sora2/sora-2-image-to-video", name: "Sora 2 Image to Video" }, + { id: "kie/veo/veo-3-1", name: "Veo 3.1" }, + { id: "kie/veo/veo-3-1-fast", name: "Veo 3.1 Fast" }, + { id: "kie/kling/kling-v2-1-master-text-to-video", name: "Kling v2.1 Master T2V" }, + { id: "kie/kling/kling-v2-1-master-image-to-video", name: "Kling v2.1 Master I2V" }, + { id: "kie/kling/v2-5-turbo-text-to-video", name: "Kling v2.5 Turbo T2V" }, + { id: "kie/kling/v2-5-turbo-image-to-video", name: "Kling v2.5 Turbo I2V" }, + { id: "kie/wan/2-7-text-to-video", name: "Wan 2.7 T2V" }, + { id: "kie/wan/2-7-image-to-video", name: "Wan 2.7 I2V" }, + { id: "kie/sora2/sora-2-text-to-video", name: "Sora 2 T2V" }, { id: "kie/hailuo/02-text-to-video-pro", name: "Hailuo 02 T2V Pro" }, - { id: "kie/hailuo/02-image-to-video-pro", name: "Hailuo 02 I2V Pro" }, { id: "kie/grok-imagine/text-to-video", name: "Grok Imagine T2V" }, - { id: "kie/grok-imagine/image-to-video", name: "Grok Imagine I2V" }, - { id: "kie/bytedance/v1-pro-text-to-video", name: "Bytedance v1 Pro T2V" }, - { id: "kie/bytedance/v1-pro-image-to-video", name: "Bytedance v1 Pro I2V" }, + { id: "kie/bytedance/v2-0-text-to-video", name: "Seedance v2.0 T2V" }, ], }, { @@ -131,9 +126,8 @@ const PROVIDER_MODELS: Record< id: "kie", name: "KIE.AI", models: [ - { id: "kie/V4", name: "Suno V4" }, - { id: "kie/V4_5", name: "Suno V4.5" }, - { id: "kie/V5", name: "Suno V5" }, + { id: "kie/suno-v3.5", name: "Suno V3.5" }, + { id: "kie/suno-v4.0", name: "Suno V4.0" }, ], }, { @@ -155,6 +149,16 @@ const PROVIDER_MODELS: Record< { id: "openai/gpt-4o-mini-tts", name: "GPT-4o Mini TTS" }, ], }, + { + id: "kie", + name: "KIE.AI", + models: [ + { id: "kie/elevenlabs/text-to-speech-multilingual-v2", name: "ElevenLabs TTS v2" }, + { id: "kie/elevenlabs/text-to-speech-turbo-2-5", name: "ElevenLabs TTS Turbo 2.5" }, + { id: "kie/elevenlabs/text-to-dialogue-v3", name: "ElevenLabs Text to Dialogue v3" }, + { id: "kie/elevenlabs/sound-effect-v2", name: "ElevenLabs Sound Effect v2" }, + ], + }, { id: "elevenlabs", name: "ElevenLabs", @@ -227,6 +231,14 @@ const PROVIDER_MODELS: Record< { id: "deepgram/base", name: "Base" }, ], }, + { + id: "kie", + name: "KIE.AI", + models: [ + { id: "kie/elevenlabs/speech-to-text", name: "ElevenLabs STT" }, + { id: "kie/elevenlabs/audio-isolation", name: "ElevenLabs Audio Isolation" }, + ], + }, { id: "assemblyai", name: "AssemblyAI ($50 free)", @@ -265,6 +277,8 @@ const PROVIDER_MODELS: Record< { id: "qwen", name: "Qwen", models: [{ id: "qwen/qwen3-asr", name: "Qwen3 ASR" }] }, ], }; +const INITIAL_IMAGE_PROVIDER = PROVIDER_MODELS.image[0]; +const INITIAL_IMAGE_MODEL = INITIAL_IMAGE_PROVIDER?.models[0]; // Voice presets per TTS provider const VOICE_PRESETS: Record = { @@ -287,6 +301,13 @@ const VOICE_PRESETS: Record = { { id: "pNInz6obpgDQGcFmaJgB", label: "Adam (EN)" }, { id: "yoZ06aMxZJJ28mfd3POQ", label: "Sam (EN)" }, ], + kie: [ + { id: "Rachel", label: "Rachel (EN)" }, + { id: "Adam", label: "Adam (EN)" }, + { id: "Brian", label: "Brian (EN)" }, + { id: "Roger", label: "Roger (EN)" }, + { id: "Bella", label: "Bella (EN)" }, + ], cartesia: [ { id: "a0e99841-438c-4a64-b679-ae501e7d6091", label: "Barbershop Man" }, { id: "694f9389-aac1-45b6-b726-9d9369183238", label: "Friendly Reading Man" }, @@ -414,8 +435,10 @@ export default function MediaPageClient() { const [prompt, setPrompt] = useState(""); // Selected provider and model per modality - const [selectedProvider, setSelectedProvider] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); + const [selectedProvider, setSelectedProvider] = useState( + INITIAL_IMAGE_PROVIDER?.id ?? "" + ); + const [selectedModel, setSelectedModel] = useState(INITIAL_IMAGE_MODEL?.id ?? ""); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); @@ -505,16 +528,6 @@ export default function MediaPageClient() { } }; - // Initialize on mount — pick first provider/model for image tab - const initialized = useRef(false); - if (!initialized.current) { - initialized.current = true; - const providers = PROVIDER_MODELS["image"] ?? []; - const firstProvider = providers[0]; - setSelectedProvider(firstProvider?.id ?? ""); - setSelectedModel(firstProvider?.models[0]?.id ?? ""); - } - const handleGenerate = async () => { setLoading(true); setError(null); diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js deleted file mode 100644 index 3595429936..0000000000 --- a/src/lib/dataPaths.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APP_NAME = void 0; -exports.getLegacyDotDataDir = getLegacyDotDataDir; -exports.getDefaultDataDir = getDefaultDataDir; -exports.resolveDataDir = resolveDataDir; -exports.isSamePath = isSamePath; -const path_1 = __importDefault(require("path")); -const os_1 = __importDefault(require("os")); -exports.APP_NAME = "omniroute"; -function fallbackHomeDir() { - const envHome = process.env.HOME || process.env.USERPROFILE; - if (typeof envHome === "string" && envHome.trim().length > 0) { - return path_1.default.resolve(envHome); - } - return os_1.default.tmpdir(); -} -function safeHomeDir() { - try { - return os_1.default.homedir(); - } - catch { - return fallbackHomeDir(); - } -} -function normalizeConfiguredPath(dir) { - if (typeof dir !== "string") - return null; - const trimmed = dir.trim(); - if (!trimmed) - return null; - return path_1.default.resolve(trimmed); -} -function getLegacyDotDataDir() { - return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`); -} -function getDefaultDataDir() { - const homeDir = safeHomeDir(); - if (process.platform === "win32") { - const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming"); - return path_1.default.join(appData, exports.APP_NAME); - } - // Support XDG on Linux/macOS when explicitly configured. - const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME); - if (xdgConfigHome) { - return path_1.default.join(xdgConfigHome, exports.APP_NAME); - } - return getLegacyDotDataDir(); -} -function resolveDataDir({ isCloud = false } = {}) { - if (isCloud) - return "/tmp"; - const configured = normalizeConfiguredPath(process.env.DATA_DIR); - if (configured) - return configured; - return getDefaultDataDir(); -} -function isSamePath(a, b) { - if (!a || !b) - return false; - const normalizedA = path_1.default.resolve(a); - const normalizedB = path_1.default.resolve(b); - if (process.platform === "win32") { - return normalizedA.toLowerCase() === normalizedB.toLowerCase(); - } - return normalizedA === normalizedB; -} diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index d1af78a9b6..67b88470f6 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -533,6 +533,55 @@ async function validateInworldProvider({ apiKey, providerSpecificData = {} }: an } } +async function validateKieProvider({ apiKey, providerSpecificData = {} }: any) { + try { + // Use credit check endpoint as requested by user based on Kie.ai docs. + const response = await validationRead("https://api.kie.ai/api/v1/chat/credit", { + method: "GET", + headers: applyCustomUserAgent( + { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + providerSpecificData + ), + }); + + if (response.ok) { + return { valid: true, error: null }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid Kie.ai API key" }; + } + + // Fallback: if credits endpoint is 404/not supported, try minimal chat probe + const chatRes = await validationWrite("https://api.kie.ai/api/v1/chat/completions", { + method: "POST", + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: JSON.stringify({ + model: providerSpecificData.validationModelId || "gpt-4o-mini", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + + if ( + chatRes.ok || + (chatRes.status >= 400 && + chatRes.status < 500 && + chatRes.status !== 401 && + chatRes.status !== 403) + ) { + return { valid: true, error: null }; + } + + return { valid: false, error: `Validation failed: ${chatRes.status}` }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} + async function validateBailianCodingPlanProvider({ apiKey, providerSpecificData = {} }: any) { try { const rawBaseUrl = @@ -1285,6 +1334,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi nanobanana: validateNanoBananaProvider, elevenlabs: validateElevenLabsProvider, inworld: validateInworldProvider, + kie: validateKieProvider, "bailian-coding-plan": validateBailianCodingPlanProvider, heroku: validateHerokuProvider, databricks: validateDatabricksProvider, diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index ef561fb77d..d15d0aceb9 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -153,6 +153,7 @@ const KNOWN_PNGS = new Set([ "glmt", "groq", "ironclaw", + "kie", "kilo-gateway", "kilocode", "kimi-coding-apikey", diff --git a/src/shared/utils/nodeRuntimeSupport.ts b/src/shared/utils/nodeRuntimeSupport.ts index 98daa345d0..21ba8c9640 100644 --- a/src/shared/utils/nodeRuntimeSupport.ts +++ b/src/shared/utils/nodeRuntimeSupport.ts @@ -11,6 +11,7 @@ export const SECURE_NODE_LINES = Object.freeze([ Object.freeze({ major: 20, minor: 20, patch: 2 }), Object.freeze({ major: 22, minor: 22, patch: 2 }), Object.freeze({ major: 24, minor: 0, patch: 0 }), + Object.freeze({ major: 25, minor: 0, patch: 0 }), ]); export const RECOMMENDED_NODE_VERSION = "24.14.1";