refactor(providers): address code review feedback for KIE provider

This commit is contained in:
wauputr4
2026-04-23 16:28:33 +00:00
parent 19bf03542c
commit 49a5a552a3
7 changed files with 97 additions and 53 deletions

View File

@@ -234,6 +234,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
kie: {
id: "kie",
baseUrl: "https://api.kie.ai/api/v1/gpt4o-image/generate",
statusUrl: "https://api.kie.ai/api/v1/gpt4o-image/record-info",
authType: "apikey",
authHeader: "bearer",
format: "kie-image",

View File

@@ -15,6 +15,7 @@ interface MusicModel {
interface MusicProvider {
id: string;
baseUrl: string;
statusUrl?: string;
authType: string;
authHeader: string;
format: string;
@@ -25,6 +26,7 @@ export const MUSIC_PROVIDERS: Record<string, MusicProvider> = {
kie: {
id: "kie",
baseUrl: "https://api.kie.ai",
statusUrl: "https://api.kie.ai/api/v1/generate/record-info",
authType: "apikey",
authHeader: "bearer",
format: "kie-music",

View File

@@ -15,6 +15,7 @@ interface VideoModel {
interface VideoProvider {
id: string;
baseUrl: string;
statusUrl?: string;
authType: string;
authHeader: string;
format: string;
@@ -25,6 +26,7 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
kie: {
id: "kie",
baseUrl: "https://api.kie.ai",
statusUrl: "https://api.kie.ai/api/v1/jobs/recordInfo",
authType: "apikey",
authHeader: "bearer",
format: "kie-video",

View File

@@ -19,6 +19,7 @@ import { randomUUID } from "crypto";
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
import { mapImageSize } from "../translator/image/sizeMapper.ts";
import { saveCallLog } from "@/lib/usageDb";
import { sleep } from "../utils/sleep.ts";
import {
submitComfyWorkflow,
pollComfyResult,
@@ -26,6 +27,15 @@ import {
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
interface KieImageOptions {
model: string;
provider: string;
providerConfig: any;
body: any;
credentials: any;
log: any;
}
const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([
"black-forest-labs/FLUX.1-redux",
"black-forest-labs/FLUX.1-depth",
@@ -296,23 +306,24 @@ async function handleKieImageGeneration({
body,
credentials,
log,
}) {
}: KieImageOptions) {
const startTime = Date.now();
const token = credentials?.apiKey || credentials?.accessToken;
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 180000);
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 300000);
const pollIntervalMs = normalizePositiveNumber(body.poll_interval_ms, 2500);
const payload: Record<string, unknown> = {
const payload = {
prompt: body.prompt,
size: typeof body.size === "string" ? body.size : "1:1",
nVariants: Number(body.n) > 0 ? Number(body.n) : 1,
image_size: mapImageSize(body.size, "1:1"),
num_images: body.n || 1,
};
try {
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("IMAGE", `${provider}/${model} (kie-image) | prompt: "${promptPreview}..."`);
}
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("IMAGE", `${provider}/${model} (kie-image) | prompt: "${promptPreview}..."`);
}
try {
const createRes = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
@@ -347,9 +358,13 @@ async function handleKieImageGeneration({
});
}
const statusUrl = providerConfig.baseUrl
.replace(/\/generate$/, "/record-info")
.replace("/api/v1/gpt4o-image/generate", "/api/v1/gpt4o-image/record-info");
// 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 deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const pollUrl = new URL(statusUrl);
@@ -442,12 +457,10 @@ async function handleKieImageGeneration({
model,
status: 502,
startTime,
error: `Image provider error: ${err.message}`,
requestBody: payload,
error: `Image provider error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
/**
* Handle Gemini-format image generation (Antigravity / Nano Banana)
* Uses Gemini's generateContent API with responseModalities: ["TEXT", "IMAGE"]
@@ -2039,10 +2052,6 @@ function normalizePositiveNumber(value, fallback) {
return Math.floor(n);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Handle SD WebUI image generation (local, no auth)
* POST {baseUrl} with { prompt, negative_prompt, width, height, steps }

View File

@@ -22,6 +22,7 @@ import {
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
import { saveCallLog } from "@/lib/usageDb";
import { sleep } from "../utils/sleep.ts";
/**
* Handle music generation request
@@ -176,6 +177,13 @@ async function handleKieMusicGeneration({
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: any;
body: any;
credentials: any;
log: any;
}) {
const startTime = Date.now();
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
@@ -216,10 +224,10 @@ async function handleKieMusicGeneration({
}
const deadline = Date.now() + timeoutMs;
const statusBaseUrl = `${baseUrl}/api/v1/generate/record-info`;
const statusUrl = providerConfig.statusUrl || `${baseUrl}/api/v1/generate/record-info`;
while (Date.now() < deadline) {
const pollUrl = new URL(statusBaseUrl);
const pollUrl = new URL(statusUrl);
pollUrl.searchParams.set("taskId", String(taskId));
const recordRes = await fetch(pollUrl.toString(), {
@@ -290,10 +298,10 @@ async function handleKieMusicGeneration({
error: `KIE music polling timed out after ${timeoutMs}ms`,
};
} catch (err) {
return { success: false, status: 502, error: `Music provider error: ${err.message}` };
return {
success: false,
status: 502,
error: `Music provider error: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -23,6 +23,7 @@ import {
extractComfyOutputFiles,
} from "../utils/comfyuiClient.ts";
import { saveCallLog } from "@/lib/usageDb";
import { sleep } from "../utils/sleep.ts";
/**
* Handle video generation request
@@ -267,6 +268,28 @@ 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 = {};
}
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[])
: [];
return urls.filter((url: unknown) => typeof url === "string" && url.length > 0);
}
async function handleKieVideoGeneration({
model,
provider,
@@ -274,6 +297,13 @@ async function handleKieVideoGeneration({
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: any;
body: any;
credentials: any;
log: any;
}) {
const startTime = Date.now();
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
@@ -318,10 +348,10 @@ async function handleKieVideoGeneration({
}
const deadline = Date.now() + timeoutMs;
const statusBaseUrl = `${baseUrl}/api/v1/jobs/recordInfo`;
const statusUrl = providerConfig.statusUrl || `${baseUrl}/api/v1/jobs/recordInfo`;
while (Date.now() < deadline) {
const pollUrl = new URL(statusBaseUrl);
const pollUrl = new URL(statusUrl);
pollUrl.searchParams.set("taskId", String(taskId));
const recordRes = await fetch(pollUrl.toString(), {
@@ -342,25 +372,8 @@ async function handleKieVideoGeneration({
).toLowerCase();
if (state === "success" || state === "1" || state === "finished") {
let resultJson: Record<string, unknown> = {};
try {
resultJson =
typeof recordData?.data?.resultJson === "string"
? JSON.parse(recordData.data.resultJson)
: recordData?.data?.resultJson || {};
} catch {
resultJson = {};
}
const urls = Array.isArray(resultJson?.resultUrls)
? resultJson.resultUrls
: Array.isArray(resultJson?.videoUrls)
? resultJson.videoUrls
: Array.isArray(recordData?.data?.response?.resultUrls)
? recordData.data.response.resultUrls
: [];
const videos = urls
.filter((url: unknown) => typeof url === "string" && url.length > 0)
.map((url: unknown) => ({ url: url as string, format: "mp4" }));
const videoUrls = normalizeKieVideoResult(recordData);
const videos = videoUrls.map((url) => ({ url, format: "mp4" }));
saveCallLog({
method: "POST",
@@ -405,10 +418,10 @@ async function handleKieVideoGeneration({
error: `KIE video polling timed out after ${timeoutMs}ms`,
};
} catch (err) {
return { success: false, status: 502, error: `Video provider error: ${err.message}` };
return {
success: false,
status: 502,
error: `Video provider error: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

9
open-sse/utils/sleep.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Shared sleep utility to pause execution for a given number of milliseconds.
*
* @param ms - Number of milliseconds to sleep
* @returns Promise that resolves after the specified time
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}