mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix: address kie provider review feedback
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import {
|
||||
isJsonObject,
|
||||
normalizeKieTaskState,
|
||||
type JsonObject,
|
||||
type KieTaskState,
|
||||
} from "../utils/kieTask.ts";
|
||||
|
||||
export type { KieTaskState } from "../utils/kieTask.ts";
|
||||
|
||||
type KieTaskInput = {
|
||||
baseUrl: string;
|
||||
@@ -16,10 +24,8 @@ type KiePollInput = {
|
||||
pollIntervalMs: number;
|
||||
};
|
||||
|
||||
export type KieTaskState = "success" | "failed" | "pending";
|
||||
|
||||
export type KieTaskRecord = {
|
||||
data: any;
|
||||
data: JsonObject;
|
||||
state: KieTaskState;
|
||||
};
|
||||
|
||||
@@ -27,45 +33,6 @@ 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" });
|
||||
@@ -79,7 +46,7 @@ export class KieExecutor extends BaseExecutor {
|
||||
return `${normalizeBaseUrl(baseUrl)}/api/v1/jobs/recordInfo`;
|
||||
}
|
||||
|
||||
async createTask({ baseUrl, token, payload, endpoint }: KieTaskInput): Promise<any> {
|
||||
async createTask({ baseUrl, token, payload, endpoint }: KieTaskInput): Promise<JsonObject> {
|
||||
const res = await fetch(this.getTaskCreateUrl(baseUrl, endpoint), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -96,7 +63,8 @@ export class KieExecutor extends BaseExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
return res.json();
|
||||
const data = (await res.json()) as unknown;
|
||||
return isJsonObject(data) ? data : {};
|
||||
}
|
||||
|
||||
async pollTask({
|
||||
@@ -124,10 +92,11 @@ export class KieExecutor extends BaseExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const state = normalizeKieTaskState(data);
|
||||
const data = (await res.json()) as unknown;
|
||||
const recordData = isJsonObject(data) ? data : {};
|
||||
const state = normalizeKieTaskState(recordData);
|
||||
if (state !== "pending") {
|
||||
return { data, state };
|
||||
return { data: recordData, state };
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
|
||||
@@ -21,6 +21,13 @@ 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";
|
||||
import {
|
||||
getKieCallbackUrl,
|
||||
getKieErrorMessage,
|
||||
getKieErrorStatus,
|
||||
isJsonObject,
|
||||
parseKieResultJson,
|
||||
} from "../utils/kieTask.ts";
|
||||
|
||||
/**
|
||||
* Return a CORS error response from an upstream fetch failure
|
||||
@@ -69,15 +76,6 @@ 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<string, string> = {
|
||||
@@ -91,17 +89,7 @@ function normalizeKieElevenLabsVoice(voice: unknown): string {
|
||||
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 {
|
||||
function findAudioUrlDeep(value: unknown): string | null {
|
||||
if (!value) return null;
|
||||
|
||||
if (typeof value === "string") {
|
||||
@@ -119,7 +107,7 @@ function findAudioUrlDeep(value: any): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (isJsonObject(value)) {
|
||||
const preferredKeys = [
|
||||
"audio_url",
|
||||
"audioUrl",
|
||||
@@ -145,16 +133,20 @@ function findAudioUrlDeep(value: any): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findKieAudioUrl(recordData: any): string | null {
|
||||
function findKieAudioUrl(recordData: unknown): string | null {
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const resultJson = parseKieResultJson(recordData);
|
||||
const response = data.response;
|
||||
const nestedData = data.data;
|
||||
const candidates = [
|
||||
recordData?.data?.response,
|
||||
recordData?.data,
|
||||
response,
|
||||
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 : []),
|
||||
...(Array.isArray(response) ? response : []),
|
||||
...(Array.isArray(nestedData) ? nestedData : []),
|
||||
...(Array.isArray(resultJson.data) ? resultJson.data : []),
|
||||
...(Array.isArray(resultJson.result) ? resultJson.result : []),
|
||||
];
|
||||
|
||||
for (const item of candidates) {
|
||||
@@ -453,13 +445,14 @@ async function handleKieAudioSpeech(providerConfig, body, modelId, token) {
|
||||
token,
|
||||
payload,
|
||||
});
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const status = getKieErrorStatus(err, 502);
|
||||
return Response.json(
|
||||
{
|
||||
error: { message: err?.message || "Kie audio createTask failed", code: err?.status || 502 },
|
||||
error: { message: getKieErrorMessage(err, "Kie audio createTask failed"), code: status },
|
||||
},
|
||||
{
|
||||
status: Number(err?.status) || 502,
|
||||
status,
|
||||
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
|
||||
}
|
||||
);
|
||||
@@ -504,10 +497,10 @@ async function pollKieAudioResult(baseUrl, modelId, taskId, token) {
|
||||
}
|
||||
return errorResponse(502, "Kie audio task completed without audio URL");
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return errorResponse(
|
||||
Number(err?.status) || 504,
|
||||
err?.message || "Kie audio generation timed out or failed"
|
||||
getKieErrorStatus(err, 504),
|
||||
getKieErrorMessage(err, "Kie audio generation timed out or failed")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -306,16 +306,20 @@ async function handleKieAudioTranscription(providerConfig, file, modelId, token)
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
typeof err === "object" && err !== null && "status" in err
|
||||
? Number((err as { status?: unknown }).status) || 502
|
||||
: 502;
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
message: err?.message || "Kie transcription createTask failed",
|
||||
code: err?.status || 502,
|
||||
message: err instanceof Error ? err.message : "Kie transcription createTask failed",
|
||||
code: status,
|
||||
},
|
||||
},
|
||||
{
|
||||
status: Number(err?.status) || 502,
|
||||
status,
|
||||
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
|
||||
}
|
||||
);
|
||||
@@ -359,10 +363,14 @@ async function pollKieTranscriptionResult(baseUrl, modelId, taskId, token) {
|
||||
{ headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const status =
|
||||
typeof err === "object" && err !== null && "status" in err
|
||||
? Number((err as { status?: unknown }).status) || 504
|
||||
: 504;
|
||||
return errorResponse(
|
||||
Number(err?.status) || 504,
|
||||
err?.message || "Kie transcription generation timed out or failed"
|
||||
status,
|
||||
err instanceof Error ? err.message : "Kie transcription generation timed out or failed"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ import { kieExecutor } from "../executors/kie.ts";
|
||||
import { mapImageSize } from "../translator/image/sizeMapper.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import {
|
||||
getKieErrorMessage,
|
||||
getKieErrorStatus,
|
||||
isJsonObject,
|
||||
parseKieResultJson,
|
||||
} from "../utils/kieTask.ts";
|
||||
import {
|
||||
submitComfyWorkflow,
|
||||
pollComfyResult,
|
||||
@@ -31,10 +37,25 @@ import {
|
||||
interface KieImageOptions {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: any;
|
||||
body: any;
|
||||
credentials: any;
|
||||
log: any;
|
||||
providerConfig: {
|
||||
baseUrl: string;
|
||||
statusUrl?: string;
|
||||
};
|
||||
body: Record<string, unknown> & {
|
||||
prompt?: unknown;
|
||||
size?: unknown;
|
||||
n?: unknown;
|
||||
timeout_ms?: unknown;
|
||||
poll_interval_ms?: unknown;
|
||||
};
|
||||
credentials?: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
} | null;
|
||||
log?: {
|
||||
info: (scope: string, message: string) => void;
|
||||
error: (scope: string, message: string) => void;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([
|
||||
@@ -300,20 +321,14 @@ export async function handleImageGeneration({ body, credentials, log, resolvedPr
|
||||
return handleOpenAIImageGeneration({ model, provider, providerConfig, body, credentials, log });
|
||||
}
|
||||
|
||||
function normalizeKieImageResult(recordData: any): string[] {
|
||||
let resultJson: Record<string, unknown> = {};
|
||||
try {
|
||||
resultJson =
|
||||
typeof recordData?.data?.resultJson === "string"
|
||||
? JSON.parse(recordData.data.resultJson)
|
||||
: recordData?.data?.resultJson || {};
|
||||
} catch {
|
||||
resultJson = {};
|
||||
}
|
||||
|
||||
function normalizeKieImageResult(recordData: unknown): string[] {
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const response = isJsonObject(data.response) ? data.response : {};
|
||||
const resultJson = parseKieResultJson(recordData);
|
||||
const urls = new Set<string>();
|
||||
|
||||
const add = (val: any) => {
|
||||
const add = (val: unknown) => {
|
||||
if (typeof val === "string" && val.startsWith("http")) urls.add(val);
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((v) => {
|
||||
@@ -329,13 +344,13 @@ function normalizeKieImageResult(recordData: any): string[] {
|
||||
add(resultJson?.imageUrl);
|
||||
|
||||
// Check data.response (common in 4o-image API)
|
||||
add(recordData?.data?.response?.resultUrls);
|
||||
add(recordData?.data?.response?.resultUrl);
|
||||
add(response.resultUrls);
|
||||
add(response.resultUrl);
|
||||
|
||||
// Check direct data fields
|
||||
add(recordData?.data?.resultImageUrls);
|
||||
add(recordData?.data?.resultImageUrl);
|
||||
add(recordData?.data?.url);
|
||||
add(data.resultImageUrls);
|
||||
add(data.resultImageUrl);
|
||||
add(data.url);
|
||||
|
||||
return Array.from(urls);
|
||||
}
|
||||
@@ -352,31 +367,44 @@ async function handleKieImageGeneration({
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 300000);
|
||||
const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, 2500);
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
const size = typeof body.size === "string" ? body.size : undefined;
|
||||
|
||||
if (!token) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 401,
|
||||
startTime,
|
||||
error: "KIE API key is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if model is a Market model (unified API)
|
||||
const fullRegistry = getImageProvider(provider);
|
||||
const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model);
|
||||
const modelEntry = fullRegistry?.models?.find((m) => m.id === model);
|
||||
const isMarket = modelEntry?.isMarket || model.includes("/");
|
||||
|
||||
const { imageUrl } = extractImageInputs(body);
|
||||
let baseUrl = "";
|
||||
let payload: any = {};
|
||||
let payload: Record<string, unknown> = {};
|
||||
|
||||
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"),
|
||||
},
|
||||
const input: Record<string, unknown> = {
|
||||
prompt,
|
||||
aspect_ratio: mapImageSize(size, "1:1"),
|
||||
};
|
||||
if (imageUrl) {
|
||||
payload.input.image_url = imageUrl;
|
||||
input.image_url = imageUrl;
|
||||
}
|
||||
payload = {
|
||||
model: marketModelId,
|
||||
input,
|
||||
};
|
||||
} else {
|
||||
// Legacy/Direct endpoint
|
||||
const modelPath = model.replace("-t2i", "").replace("-i2i", "");
|
||||
@@ -385,8 +413,8 @@ async function handleKieImageGeneration({
|
||||
: `https://api.kie.ai/api/v1/${modelPath}/generate`;
|
||||
|
||||
payload = {
|
||||
prompt: body.prompt,
|
||||
image_size: mapImageSize(body.size, "1:1"),
|
||||
prompt,
|
||||
image_size: mapImageSize(size, "1:1"),
|
||||
num_images: body.n || 1,
|
||||
};
|
||||
}
|
||||
@@ -436,106 +464,58 @@ async function handleKieImageGeneration({
|
||||
? providerConfig.statusUrl
|
||||
: baseUrl.replace(/\/generate$/, "/record-info");
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const pollUrl = new URL(statusUrl);
|
||||
pollUrl.searchParams.set("taskId", String(taskId));
|
||||
const { data: recordData, state } = await kieExecutor.pollTask({
|
||||
statusUrl,
|
||||
taskId: String(taskId),
|
||||
token,
|
||||
timeoutMs,
|
||||
pollIntervalMs,
|
||||
});
|
||||
|
||||
const recordRes = await fetch(pollUrl.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
if (state === "success") {
|
||||
if (log) {
|
||||
log.info("IMAGE", `KIE poll success for task ${taskId}`);
|
||||
}
|
||||
const urls = normalizeKieImageResult(recordData);
|
||||
const images = urls.map((url: string) => ({ url, revised_prompt: prompt }));
|
||||
|
||||
return saveImageSuccessResult({
|
||||
provider,
|
||||
model,
|
||||
startTime,
|
||||
requestBody: payload,
|
||||
responseBody: { images_count: images.length },
|
||||
images,
|
||||
});
|
||||
}
|
||||
|
||||
if (!recordRes.ok) {
|
||||
const errorText = await recordRes.text();
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: recordRes.status,
|
||||
startTime,
|
||||
error: errorText,
|
||||
requestBody: payload,
|
||||
});
|
||||
}
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const recordDataBody = isJsonObject(record.data) ? record.data : {};
|
||||
const errorMessage =
|
||||
recordDataBody.errorMessage ||
|
||||
recordDataBody.failMsg ||
|
||||
record.msg ||
|
||||
"KIE image task failed";
|
||||
|
||||
const recordData = await recordRes.json();
|
||||
const state = String(
|
||||
recordData?.data?.status ??
|
||||
recordData?.data?.state ??
|
||||
recordData?.data?.successFlag ??
|
||||
recordData?.msg ??
|
||||
"PENDING"
|
||||
).toUpperCase();
|
||||
|
||||
if (state === "SUCCESS" || state === "1" || state === "FINISHED") {
|
||||
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,
|
||||
startTime,
|
||||
requestBody: payload,
|
||||
responseBody: { images_count: images.length },
|
||||
images,
|
||||
});
|
||||
}
|
||||
|
||||
// Expanded failure state detection
|
||||
if (
|
||||
state === "FAIL" ||
|
||||
state === "FAILED" ||
|
||||
state === "ERROR" ||
|
||||
state === "2" ||
|
||||
state === "3" ||
|
||||
state.includes("FAIL") ||
|
||||
state.includes("ERROR") ||
|
||||
state === "CREATE_TASK_FAILED" ||
|
||||
state === "GENERATE_FAILED"
|
||||
) {
|
||||
const errorMessage =
|
||||
recordData?.data?.errorMessage ||
|
||||
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,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: errorMessage,
|
||||
requestBody: payload,
|
||||
});
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
if (log) {
|
||||
log.error("IMAGE", `KIE poll failed for task ${taskId}: ${JSON.stringify(recordData)}`);
|
||||
}
|
||||
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 504,
|
||||
status: 502,
|
||||
startTime,
|
||||
error: `KIE image polling timed out after ${timeoutMs}ms`,
|
||||
error: String(errorMessage),
|
||||
requestBody: payload,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
model,
|
||||
status: 502,
|
||||
status: getKieErrorStatus(err, 502),
|
||||
startTime,
|
||||
error: `Image provider error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
error: `Image provider error: ${getKieErrorMessage(err, "KIE image generation failed")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,16 +23,7 @@ import {
|
||||
extractComfyOutputFiles,
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
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"
|
||||
);
|
||||
}
|
||||
import { getKieCallbackUrl, isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
|
||||
|
||||
function normalizeKieSunoModel(model: string): string {
|
||||
const map: Record<string, string> = {
|
||||
@@ -42,42 +33,39 @@ function normalizeKieSunoModel(model: string): string {
|
||||
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[] {
|
||||
function normalizeKieMusicTracks(recordData: unknown): Array<Record<string, unknown>> {
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const response = isJsonObject(data.response) ? data.response : {};
|
||||
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,
|
||||
response.sunoData,
|
||||
response.data,
|
||||
data.data,
|
||||
data.sunoData,
|
||||
resultJson.sunoData,
|
||||
resultJson.data,
|
||||
resultJson.result,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (Array.isArray(candidate) && candidate.length > 0) {
|
||||
return candidate;
|
||||
return candidate
|
||||
.map((track) =>
|
||||
isJsonObject(track) ? track : typeof track === "string" ? { audioUrl: track } : null
|
||||
)
|
||||
.filter((track): track is Record<string, unknown> => track !== null);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
response.audioUrl ||
|
||||
response.audio_url ||
|
||||
data.resultUrl ||
|
||||
data.audio_url ||
|
||||
resultJson.audioUrl ||
|
||||
resultJson.audio_url ||
|
||||
resultJson.url;
|
||||
|
||||
return typeof singleUrl === "string" && singleUrl.length > 0 ? [{ audioUrl: singleUrl }] : [];
|
||||
}
|
||||
@@ -238,24 +226,42 @@ async function handleKieMusicGeneration({
|
||||
}: {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: any;
|
||||
body: any;
|
||||
credentials: any;
|
||||
log: any;
|
||||
providerConfig: {
|
||||
baseUrl: string;
|
||||
statusUrl?: string;
|
||||
};
|
||||
body: Record<string, unknown> & {
|
||||
prompt?: unknown;
|
||||
timeout_ms?: unknown;
|
||||
poll_interval_ms?: unknown;
|
||||
};
|
||||
credentials?: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
} | null;
|
||||
log?: {
|
||||
info: (scope: string, message: string) => void;
|
||||
error: (scope: string, message: string) => void;
|
||||
} | null;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
|
||||
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 prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
|
||||
if (!token) {
|
||||
return { success: false, status: 401, error: "KIE API key is required" };
|
||||
}
|
||||
|
||||
// Check if model is a Market model
|
||||
const fullRegistry = getMusicProvider(provider);
|
||||
const modelEntry = fullRegistry?.models?.find((m: any) => m.id === model);
|
||||
const modelEntry = fullRegistry?.models?.find((m) => m.id === model);
|
||||
const isMarket = modelEntry?.isMarket || model.includes("/");
|
||||
|
||||
let url = "";
|
||||
let payload: any = {};
|
||||
let payload: Record<string, unknown> = {};
|
||||
|
||||
if (isMarket) {
|
||||
url = `${baseUrl}/api/v1/jobs/createTask`;
|
||||
@@ -263,14 +269,14 @@ async function handleKieMusicGeneration({
|
||||
model: model.includes("/") ? model.split("/").pop() : model,
|
||||
callBackUrl: getKieCallbackUrl(body),
|
||||
input: {
|
||||
prompt: body.prompt,
|
||||
prompt,
|
||||
instrumental: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
url = `${baseUrl}/api/v1/generate`;
|
||||
payload = {
|
||||
prompt: body.prompt,
|
||||
prompt,
|
||||
customMode: false,
|
||||
instrumental: true,
|
||||
model: normalizeKieSunoModel(model),
|
||||
@@ -302,92 +308,58 @@ async function handleKieMusicGeneration({
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
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);
|
||||
pollUrl.searchParams.set("taskId", String(taskId));
|
||||
const { data: recordData, state } = await kieExecutor.pollTask({
|
||||
statusUrl,
|
||||
taskId: String(taskId),
|
||||
token,
|
||||
timeoutMs,
|
||||
pollIntervalMs,
|
||||
});
|
||||
|
||||
const recordRes = await fetch(pollUrl.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (state === "success") {
|
||||
const tracks = normalizeKieMusicTracks(recordData);
|
||||
|
||||
if (!recordRes.ok) {
|
||||
const errorText = await recordRes.text();
|
||||
return { success: false, status: recordRes.status, error: errorText };
|
||||
}
|
||||
const audioFiles = tracks
|
||||
.map((track) =>
|
||||
typeof track.audioUrl === "string"
|
||||
? track.audioUrl
|
||||
: typeof track.audio_url === "string"
|
||||
? track.audio_url
|
||||
: typeof track.url === "string"
|
||||
? track.url
|
||||
: null
|
||||
)
|
||||
.filter((url): url is string => typeof url === "string" && url.length > 0)
|
||||
.map((url: string) => ({ url, format: "mp3" }));
|
||||
|
||||
const recordData = await recordRes.json();
|
||||
const state = String(
|
||||
recordData?.data?.status ??
|
||||
recordData?.data?.state ??
|
||||
recordData?.data?.successFlag ??
|
||||
recordData?.msg ??
|
||||
"PENDING"
|
||||
).toUpperCase();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/music/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { audio_count: audioFiles.length },
|
||||
}).catch(() => {});
|
||||
|
||||
if (state === "SUCCESS" || state === "1" || state === "FINISHED") {
|
||||
const tracks = normalizeKieMusicTracks(recordData);
|
||||
|
||||
const audioFiles = tracks
|
||||
.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" }));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/music/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { audio_count: audioFiles.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: audioFiles },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
state.includes("FAIL") ||
|
||||
state.includes("ERROR") ||
|
||||
state === "2" ||
|
||||
state === "3" ||
|
||||
state === "CREATE_TASK_FAILED" ||
|
||||
state === "GENERATE_AUDIO_FAILED"
|
||||
) {
|
||||
const errorMessage =
|
||||
recordData?.data?.errorMessage ||
|
||||
recordData?.data?.failMsg ||
|
||||
recordData?.msg ||
|
||||
`KIE music task failed with status: ${state}`;
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: audioFiles },
|
||||
};
|
||||
}
|
||||
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const errorMessage = data.errorMessage || data.failMsg || record.msg || "KIE music task failed";
|
||||
return { success: false, status: 502, error: String(errorMessage) };
|
||||
} catch (err: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
status: 504,
|
||||
error: `KIE music polling timed out after ${timeoutMs}ms`,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
|
||||
error: `Music provider error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts";
|
||||
import { kieExecutor } from "../executors/kie.ts";
|
||||
import { isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
|
||||
import {
|
||||
submitComfyWorkflow,
|
||||
pollComfyResult,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
extractComfyOutputFiles,
|
||||
} from "../utils/comfyuiClient.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
|
||||
/**
|
||||
* Handle video generation request
|
||||
@@ -269,23 +269,18 @@ async function handleSDWebUIVideoGeneration({ model, provider, providerConfig, b
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKieVideoResult(recordData: any): string[] {
|
||||
let resultJson: Record<string, unknown> = {};
|
||||
try {
|
||||
resultJson =
|
||||
typeof recordData?.data?.resultJson === "string"
|
||||
? JSON.parse(recordData.data.resultJson)
|
||||
: recordData?.data?.resultJson || {};
|
||||
} catch {
|
||||
resultJson = {};
|
||||
}
|
||||
function normalizeKieVideoResult(recordData: unknown): string[] {
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const response = isJsonObject(data.response) ? data.response : {};
|
||||
const resultJson = parseKieResultJson(recordData);
|
||||
|
||||
const urls = Array.isArray(resultJson?.resultUrls)
|
||||
? (resultJson.resultUrls as string[])
|
||||
: Array.isArray(resultJson?.videoUrls)
|
||||
? (resultJson.videoUrls as string[])
|
||||
: Array.isArray(recordData?.data?.response?.resultUrls)
|
||||
? (recordData.data.response.resultUrls as string[])
|
||||
: Array.isArray(response.resultUrls)
|
||||
? (response.resultUrls as string[])
|
||||
: [];
|
||||
|
||||
return urls.filter((url: unknown) => typeof url === "string" && url.length > 0);
|
||||
@@ -301,16 +296,37 @@ async function handleKieVideoGeneration({
|
||||
}: {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: any;
|
||||
body: any;
|
||||
credentials: any;
|
||||
log: any;
|
||||
providerConfig: {
|
||||
baseUrl: string;
|
||||
statusUrl?: string;
|
||||
};
|
||||
body: Record<string, unknown> & {
|
||||
prompt?: unknown;
|
||||
duration?: unknown;
|
||||
aspect_ratio?: unknown;
|
||||
sound?: unknown;
|
||||
timeout_ms?: unknown;
|
||||
poll_interval_ms?: unknown;
|
||||
};
|
||||
credentials?: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
} | null;
|
||||
log?: {
|
||||
info: (scope: string, message: string) => void;
|
||||
error: (scope: string, message: string) => void;
|
||||
} | null;
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
|
||||
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 prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
|
||||
|
||||
if (!token) {
|
||||
return { success: false, status: 401, error: "KIE API key is required" };
|
||||
}
|
||||
|
||||
// Strip category prefix (e.g., "veo/veo-3-1" -> "veo-3-1")
|
||||
const marketModelId = model.includes("/") ? model.split("/").pop() : model;
|
||||
@@ -318,9 +334,9 @@ async function handleKieVideoGeneration({
|
||||
const payload = {
|
||||
model: marketModelId,
|
||||
input: {
|
||||
prompt: body.prompt,
|
||||
prompt,
|
||||
duration: body.duration ? String(body.duration) : "5",
|
||||
aspect_ratio: body.aspect_ratio || "16:9",
|
||||
aspect_ratio: typeof body.aspect_ratio === "string" ? body.aspect_ratio : "16:9",
|
||||
sound: body.sound === true,
|
||||
},
|
||||
};
|
||||
@@ -345,80 +361,44 @@ async function handleKieVideoGeneration({
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const statusUrl = providerConfig.statusUrl || `${baseUrl}/api/v1/jobs/recordInfo`;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const pollUrl = new URL(statusUrl);
|
||||
pollUrl.searchParams.set("taskId", String(taskId));
|
||||
const { data: recordData, state } = await kieExecutor.pollTask({
|
||||
statusUrl,
|
||||
taskId: String(taskId),
|
||||
token,
|
||||
timeoutMs,
|
||||
pollIntervalMs,
|
||||
});
|
||||
|
||||
const recordRes = await fetch(pollUrl.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (state === "success") {
|
||||
const videoUrls = normalizeKieVideoResult(recordData);
|
||||
const videos = videoUrls.map((url) => ({ url, format: "mp4" }));
|
||||
|
||||
if (!recordRes.ok) {
|
||||
const errorText = await recordRes.text();
|
||||
return { success: false, status: recordRes.status, error: errorText };
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { videos_count: videos.length },
|
||||
}).catch(() => {});
|
||||
|
||||
const recordData = await recordRes.json();
|
||||
const state = String(
|
||||
recordData?.data?.state || recordData?.data?.status || "generating"
|
||||
).toLowerCase();
|
||||
|
||||
if (state === "success" || state === "1" || state === "finished") {
|
||||
const videoUrls = normalizeKieVideoResult(recordData);
|
||||
const videos = videoUrls.map((url) => ({ url, format: "mp4" }));
|
||||
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
responseBody: { videos_count: videos.length },
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: videos },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
state === "fail" ||
|
||||
state === "failed" ||
|
||||
state === "error" ||
|
||||
state === "2" ||
|
||||
state === "3" ||
|
||||
state.includes("fail") ||
|
||||
state.includes("error") ||
|
||||
state.includes("failed")
|
||||
) {
|
||||
const errorMessage =
|
||||
recordData?.data?.failMsg ||
|
||||
recordData?.data?.errorMessage ||
|
||||
recordData?.msg ||
|
||||
`KIE video task failed with state: ${state}`;
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
return {
|
||||
success: true,
|
||||
data: { created: Math.floor(Date.now() / 1000), data: videos },
|
||||
};
|
||||
}
|
||||
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const errorMessage = data.failMsg || data.errorMessage || record.msg || "KIE video task failed";
|
||||
return { success: false, status: 502, error: String(errorMessage) };
|
||||
} catch (err: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
status: 504,
|
||||
error: `KIE video polling timed out after ${timeoutMs}ms`,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
status: 502,
|
||||
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
|
||||
error: `Video provider error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
};
|
||||
}
|
||||
|
||||
97
open-sse/utils/kieTask.ts
Normal file
97
open-sse/utils/kieTask.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
|
||||
export type KieTaskState = "success" | "failed" | "pending";
|
||||
|
||||
export type KieCallbackBody = {
|
||||
callBackUrl?: unknown;
|
||||
callback_url?: unknown;
|
||||
callbackUrl?: unknown;
|
||||
};
|
||||
|
||||
export function isJsonObject(value: unknown): value is JsonObject {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function getKieCallbackUrl(body: KieCallbackBody = {}): string {
|
||||
const callbackUrl = body.callBackUrl ?? body.callback_url ?? body.callbackUrl;
|
||||
return typeof callbackUrl === "string" && callbackUrl.trim().length > 0
|
||||
? callbackUrl
|
||||
: "https://omniroute.local/api/kie/callback";
|
||||
}
|
||||
|
||||
export function parseKieResultJson(recordData: unknown): JsonObject {
|
||||
const data = isJsonObject(recordData) && isJsonObject(recordData.data) ? recordData.data : {};
|
||||
const resultJson = data.resultJson;
|
||||
|
||||
if (typeof resultJson === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(resultJson) as unknown;
|
||||
return isJsonObject(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return isJsonObject(resultJson) ? resultJson : {};
|
||||
}
|
||||
|
||||
export function normalizeKieTaskState(recordData: unknown): KieTaskState {
|
||||
const record = isJsonObject(recordData) ? recordData : {};
|
||||
const data = isJsonObject(record.data) ? record.data : {};
|
||||
const state = String(
|
||||
data.status ?? data.state ?? data.successFlag ?? record.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" ||
|
||||
state === "GENERATE_AUDIO_FAILED"
|
||||
) {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
return "pending";
|
||||
}
|
||||
|
||||
export function getKieErrorStatus(error: unknown, fallback = 502): number {
|
||||
if (isJsonObject(error)) {
|
||||
const status = Number(error.status);
|
||||
if (Number.isFinite(status) && status > 0) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function getKieErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (isJsonObject(error) && typeof error.message === "string" && error.message.length > 0) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return typeof error === "string" && error.length > 0 ? error : fallback;
|
||||
}
|
||||
@@ -279,7 +279,7 @@ async function validateDirectChatProvider({ url, headers, body, providerSpecific
|
||||
}
|
||||
|
||||
return { valid: false, error: `Validation failed: ${response.status}` };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
@@ -577,7 +577,7 @@ async function validateKieProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
}
|
||||
|
||||
return { valid: false, error: `Validation failed: ${chatRes.status}` };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user