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:
Diego Rodrigues de Sa e Souza
2026-08-29 08:10:13 -03:00
committed by GitHub
parent c705147de2
commit d2ad71cf56
3 changed files with 178 additions and 19 deletions

View File

@@ -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).

View File

@@ -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,

View File

@@ -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"
);
});