mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
fix(kie): route flux/kontext to its dedicated endpoint, not the Market createTask flow (#11296) (#11985)
flux/kontext is catalogued with isMarket: true, so handleKieImageGeneration routed it through KIE's unified Market createTask endpoint with model: "flux/kontext". KIE does not expose Flux Kontext through the Market catalog at all -- it lives under a dedicated API tree (POST /api/v1/flux/kontext/generate, poll GET /api/v1/flux/kontext/record-info, models flux-kontext-pro/flux-kontext-max) -- so the Market endpoint rejected it with "model name not supported", matching the reporter's exact error text. Special-case flux/kontext ahead of the isMarket branch so it hits the dedicated endpoint/payload shape instead of being treated as a Market entry. z-image/4.0-*/4.5-* remains intentionally untouched (still blocked on reporter/live confirmation per the existing in-code comment). Co-authored-by: Markus Hartung <mail@hartmark.se>
This commit is contained in:
committed by
GitHub
parent
c705147de2
commit
d2ad71cf56
@@ -0,0 +1 @@
|
||||
- **fix(kie):** reroute `flux/kontext` off the KIE Market `createTask` flow — it is catalogued with `isMarket: true` but has no Market catalog page, so KIE rejected it with "model name not supported"; it now hits the dedicated `POST /api/v1/flux/kontext/generate` / `GET /api/v1/flux/kontext/record-info` endpoints instead (#11296).
|
||||
@@ -119,20 +119,21 @@ interface KieImageOptions {
|
||||
// ideogram/v3-reframe has no dedicated docs.kie.ai page as of this sweep
|
||||
// (its 3 siblings above are all direct id matches, so it is assumed
|
||||
// correct by pattern, not independently confirmed).
|
||||
// Two catalog entries remain UNRESOLVED after this sweep and are
|
||||
// deliberately left untouched pending a follow-up (see #11296 discussion):
|
||||
// One catalog entry remains UNRESOLVED after this sweep and is deliberately
|
||||
// left untouched pending a follow-up (see #11296 discussion):
|
||||
// - z-image/4.0-text-to-image and z-image/4.5-text-to-image: the only
|
||||
// documented Z-Image Market page (docs.kie.ai/market/z-image/z-image)
|
||||
// shows a single fixed `model` enum value `"z-image"` with no
|
||||
// version-specific id or "version" input field found — unclear whether
|
||||
// both catalog ids should collapse to the same upstream call.
|
||||
// - flux/kontext: no `docs.kie.ai/market/flux2/kontext` (or similar)
|
||||
// Market page exists; Flux Kontext is documented under the separate
|
||||
// `/flux-kontext-api/*` docs tree with its own endpoint
|
||||
// (`POST /api/v1/flux/kontext/generate`, models `flux-kontext-pro`/
|
||||
// `flux-kontext-max`), not the Market `createTask` flow this map feeds.
|
||||
// This entry may be miscatalogued as `isMarket: true` and need a
|
||||
// dedicated reroute rather than an id rewrite.
|
||||
// flux/kontext is RESOLVED (#11296): it is catalogued with `isMarket: true`
|
||||
// but has no `docs.kie.ai/market/flux2/kontext` (or similar) Market page —
|
||||
// Flux Kontext is documented under the separate `/flux-kontext-api/*` docs
|
||||
// tree with its own endpoint (`POST /api/v1/flux/kontext/generate`, poll
|
||||
// `GET /api/v1/flux/kontext/record-info`, models `flux-kontext-pro`/
|
||||
// `flux-kontext-max`), not the Market `createTask` flow this map feeds. It is
|
||||
// NOT in KIE_MARKET_UPSTREAM_MODEL_IDS below on purpose — handleKieImageGeneration
|
||||
// reroutes it to the dedicated endpoint instead of rewriting its id.
|
||||
export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap<string, string> = new Map([
|
||||
["google-imagen/nano-banana", "google/nano-banana"],
|
||||
["google-imagen/nano-banana-2", "nano-banana-2"],
|
||||
@@ -815,13 +816,29 @@ async function handleKieImageGeneration({
|
||||
// Check if model is a Market model (unified API)
|
||||
const fullRegistry = getImageProvider(provider);
|
||||
const modelEntry = fullRegistry?.models?.find((m) => m.id === model);
|
||||
const isMarket = modelEntry?.isMarket || model.includes("/");
|
||||
// #11296 — flux/kontext is catalogued with `isMarket: true`, but KIE does not
|
||||
// expose it through the Market catalog at all: it lives under a dedicated API
|
||||
// tree (POST /api/v1/flux/kontext/generate, poll .../flux/kontext/record-info)
|
||||
// that rejects the Market createTask flow with "model name not supported". Route
|
||||
// it there instead of treating it as a Market entry (see KIE_MARKET_UPSTREAM_MODEL_IDS
|
||||
// comment above for the same finding).
|
||||
const isFluxKontext = model === "flux/kontext";
|
||||
const isMarket = !isFluxKontext && (modelEntry?.isMarket || model.includes("/"));
|
||||
|
||||
const { imageUrl } = extractImageInputs(body);
|
||||
let baseUrl = "";
|
||||
let payload: Record<string, unknown> = {};
|
||||
|
||||
if (isMarket) {
|
||||
if (isFluxKontext) {
|
||||
// Dedicated Flux Kontext API endpoint (not part of the Market catalog).
|
||||
baseUrl = `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/flux/kontext/generate`;
|
||||
payload = {
|
||||
prompt,
|
||||
aspectRatio: mapImageSize(size),
|
||||
model: "flux-kontext-pro",
|
||||
...(imageUrl ? { inputImage: imageUrl } : {}),
|
||||
};
|
||||
} else if (isMarket) {
|
||||
// Unified Market API endpoint
|
||||
baseUrl = `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/createTask`;
|
||||
const input: Record<string, unknown> = {
|
||||
@@ -853,13 +870,18 @@ async function handleKieImageGeneration({
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log.info(
|
||||
"IMAGE",
|
||||
`${provider}/${model} (${isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."`
|
||||
`${provider}/${model} (${isFluxKontext ? "flux-kontext" : isMarket ? "market" : "direct"}) | prompt: "${promptPreview}..."`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = isMarket ? "/api/v1/jobs/createTask" : new URL(baseUrl).pathname;
|
||||
const createBaseUrl = isMarket ? providerConfig.baseUrl : baseUrl.replace(endpoint, "");
|
||||
const endpoint = isFluxKontext
|
||||
? "/api/v1/flux/kontext/generate"
|
||||
: isMarket
|
||||
? "/api/v1/jobs/createTask"
|
||||
: new URL(baseUrl).pathname;
|
||||
const createBaseUrl =
|
||||
isFluxKontext || isMarket ? providerConfig.baseUrl : baseUrl.replace(endpoint, "");
|
||||
const createData = await kieExecutor.createTask({
|
||||
baseUrl: createBaseUrl,
|
||||
token,
|
||||
@@ -888,11 +910,13 @@ async function handleKieImageGeneration({
|
||||
}
|
||||
|
||||
// Use statusUrl from providerConfig if available, fallback to dynamic derivation
|
||||
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 statusUrl = isFluxKontext
|
||||
? `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/flux/kontext/record-info`
|
||||
: isMarket
|
||||
? `${providerConfig.baseUrl.replace(/\/$/, "")}/api/v1/jobs/recordInfo`
|
||||
: providerConfig.statusUrl && !providerConfig.statusUrl.includes("jobs/recordInfo")
|
||||
? providerConfig.statusUrl
|
||||
: baseUrl.replace(/\/generate$/, "/record-info");
|
||||
|
||||
const { data: recordData, state } = await kieExecutor.pollTask({
|
||||
statusUrl,
|
||||
|
||||
@@ -410,3 +410,137 @@ test("KIE direct image routing keeps the gpt4o-image endpoint and payload shape"
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
// #11296 — flux/kontext is catalogued with `isMarket: true`, but KIE does not
|
||||
// expose it through the Market catalog: it lives under a dedicated API tree
|
||||
// (POST /api/v1/flux/kontext/generate, GET /api/v1/flux/kontext/record-info).
|
||||
// Sending it through the Market createTask flow gets rejected with "model
|
||||
// name not supported" -- these tests lock in the dedicated-endpoint reroute
|
||||
// and guard against a future regression back to the Market flow.
|
||||
|
||||
test("KIE flux/kontext routes to the dedicated Flux Kontext endpoint, never the Market createTask endpoint (#11296)", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let createUrl = "";
|
||||
let createBody: Record<string, unknown> | undefined;
|
||||
let pollUrl = "";
|
||||
|
||||
globalThis.fetch = (async (url: unknown, options: { body?: unknown } = {}) => {
|
||||
const stringUrl = String(url);
|
||||
|
||||
if (stringUrl === "https://api.kie.ai/api/v1/flux/kontext/generate") {
|
||||
createUrl = stringUrl;
|
||||
createBody = JSON.parse(String(options.body ?? "{}")) as Record<string, unknown>;
|
||||
return new Response(JSON.stringify({ code: 200, data: { taskId: "kie-flux-kontext-1" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (stringUrl.startsWith("https://api.kie.ai/api/v1/flux/kontext/record-info")) {
|
||||
pollUrl = stringUrl;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
data: {
|
||||
state: "success",
|
||||
resultJson: JSON.stringify({
|
||||
resultUrls: ["https://example.com/kie-flux-kontext-image.png"],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// Any other URL (in particular the Market createTask/recordInfo endpoints)
|
||||
// is the regression this test guards against.
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "kie/flux/kontext",
|
||||
prompt: "a calm harbour at sunrise",
|
||||
size: "1024x1024",
|
||||
n: 1,
|
||||
},
|
||||
credentials: { apiKey: "test-kie-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true, "KIE flux/kontext generation should succeed");
|
||||
assert.equal(createUrl, "https://api.kie.ai/api/v1/flux/kontext/generate");
|
||||
assert.deepEqual(createBody, {
|
||||
prompt: "a calm harbour at sunrise",
|
||||
aspectRatio: "1:1",
|
||||
model: "flux-kontext-pro",
|
||||
});
|
||||
assert.equal(new URL(pollUrl).searchParams.get("taskId"), "kie-flux-kontext-1");
|
||||
assert.ok("data" in result, "successful KIE generation must return image data");
|
||||
assert.equal(result.data.data[0].url, "https://example.com/kie-flux-kontext-image.png");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("KIE flux/kontext forwards an input image as 'inputImage' for edit calls (#11296)", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let createBody: Record<string, unknown> | undefined;
|
||||
|
||||
globalThis.fetch = (async (url: unknown, options: { body?: unknown } = {}) => {
|
||||
const stringUrl = String(url);
|
||||
|
||||
if (stringUrl === "https://api.kie.ai/api/v1/flux/kontext/generate") {
|
||||
createBody = JSON.parse(String(options.body ?? "{}")) as Record<string, unknown>;
|
||||
return new Response(JSON.stringify({ code: 200, data: { taskId: "kie-flux-kontext-2" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (stringUrl.startsWith("https://api.kie.ai/api/v1/flux/kontext/record-info")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
data: {
|
||||
state: "success",
|
||||
resultJson: JSON.stringify({
|
||||
resultUrls: ["https://example.com/kie-flux-kontext-edit.png"],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "kie/flux/kontext",
|
||||
prompt: "add a lighthouse",
|
||||
size: "1024x1024",
|
||||
n: 1,
|
||||
image: "https://example.com/source.png",
|
||||
},
|
||||
credentials: { apiKey: "test-kie-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(createBody?.inputImage, "https://example.com/source.png");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("flux/kontext is not part of the KIE Market upstream id map (#11296)", () => {
|
||||
assert.equal(
|
||||
KIE_MARKET_UPSTREAM_MODEL_IDS.has("flux/kontext"),
|
||||
false,
|
||||
"flux/kontext is rerouted to a dedicated endpoint, not id-rewritten through the Market map"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user