diff --git a/changelog.d/fixes/11296-kie-market-model-id-sweep.md b/changelog.d/fixes/11296-kie-market-model-id-sweep.md new file mode 100644 index 0000000000..888a96c3d5 --- /dev/null +++ b/changelog.d/fixes/11296-kie-market-model-id-sweep.md @@ -0,0 +1 @@ +- **fix(kie):** correct 12 more KIE Market catalog ids that were sent to `createTask` unchanged but diverge from KIE's documented upstream `model` values — GPT Image 2 T2I/I2I (drops the `gpt/` prefix), GPT Image 1.5 T2I/I2I (`gpt-image/` namespace), Seedream 5.0 Lite T2I/I2I (drops the `.0`), all 4 Flux 2 variants (`flux-2/` namespace, generic variant renamed `flex`), and Wan 2.7 Image / Image Pro (dash instead of dot) — each verified individually against the literal example request published on docs.kie.ai. `#11326`'s "everything else already matches" claim was wrong a second time (#11296); `z-image/4.0-*`/`z-image/4.5-*` and `flux/kontext` remain open, documented as unresolved in `KIE_MARKET_UPSTREAM_MODEL_IDS`'s comment pending further verification. diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index db0745898e..3a16c897f2 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -92,19 +92,65 @@ interface KieImageOptions { } // KIE Market catalog ids are namespaced for OmniRoute's catalog -// (`google-imagen/`), but the KIE Market createTask API expects +// (`/`), but the KIE Market createTask API expects // vendor-specific upstream ids that do not follow a single consistent -// pattern (confirmed against docs.kie.ai/market/google/* — see #11225, -// #11296): nano-banana-2 and nano-banana-pro drop the vendor namespace -// entirely, while nano-banana and nano-banana-edit use a `google/` prefix -// instead of `google-imagen/`. Every other KIE Market namespace (seedream, -// flux, ideogram, qwen, wan, grok-imagine, gpt) already matches its real -// upstream id byte-for-byte, so this map stays scoped to google-imagen. +// pattern. Every entry below was confirmed individually against the literal +// example request JSON published on docs.kie.ai (never inferred by pattern — +// see #11326's false "everything else already matches" claim and #11296's +// follow-up correction): +// - google-imagen: nano-banana-2 and nano-banana-pro drop the vendor +// namespace entirely; nano-banana and nano-banana-edit use a `google/` +// prefix instead of `google-imagen/` (docs.kie.ai/market/google/*). +// - gpt: gpt-image-2-* drops the `gpt/` namespace entirely +// (docs.kie.ai/market/gpt/gpt-image-2-*); gpt-image-1.5-* uses a +// `gpt-image/` namespace instead of `gpt/gpt-image-1.5-`, and keeps the +// dot in "1.5" (docs.kie.ai/market/gpt-image/1-5-*). +// - seedream: 5.0-lite-* drops the ".0" — real id is `5-lite-*` +// (docs.kie.ai/market/seedream/5-lite-text-to-image); seedream 4.5 (T2I +// and edit) already matches byte-for-byte. +// - flux: `flux/2-*` uses a `flux-2/` namespace (dash, not slash); the +// generic (non-"pro") variant is named `flex` upstream, not `2` +// (docs.kie.ai/market/flux2/pro-*, .../flex-*). +// - wan: `wan/2.7-*` keeps the dot in our catalog, but KIE's documented +// enum uses a dash — real id is `wan/2-7-*` +// (docs.kie.ai/market/wan/2-7-image[-pro]). +// - ideogram (v3-text-to-image, v3-edit, v3-remix), qwen, qwen2, and +// grok-imagine already match byte-for-byte +// (docs.kie.ai/market/{ideogram,qwen,qwen2,grok-imagine}/*). +// 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): +// - 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. export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap = new Map([ ["google-imagen/nano-banana", "google/nano-banana"], ["google-imagen/nano-banana-2", "nano-banana-2"], ["google-imagen/nano-banana-pro", "nano-banana-pro"], ["google-imagen/nano-banana-edit", "google/nano-banana-edit"], + ["gpt/gpt-image-2-text-to-image", "gpt-image-2-text-to-image"], + ["gpt/gpt-image-2-image-to-image", "gpt-image-2-image-to-image"], + ["gpt/gpt-image-1.5-text-to-image", "gpt-image/1.5-text-to-image"], + ["gpt/gpt-image-1.5-image-to-image", "gpt-image/1.5-image-to-image"], + ["seedream/5.0-lite-text-to-image", "seedream/5-lite-text-to-image"], + ["seedream/5.0-lite-image-to-image", "seedream/5-lite-image-to-image"], + ["flux/2-pro-text-to-image", "flux-2/pro-text-to-image"], + ["flux/2-pro-image-to-image", "flux-2/pro-image-to-image"], + ["flux/2-text-to-image", "flux-2/flex-text-to-image"], + ["flux/2-image-to-image", "flux-2/flex-image-to-image"], + ["wan/2.7-image", "wan/2-7-image"], + ["wan/2.7-image-pro", "wan/2-7-image-pro"], ]); export function resolveKieMarketUpstreamModelId(publicModelId: string): string { diff --git a/tests/unit/kie-market-upstream-id-11296-probe.test.ts b/tests/unit/kie-market-upstream-id-11296-probe.test.ts new file mode 100644 index 0000000000..5de9f17bfe --- /dev/null +++ b/tests/unit/kie-market-upstream-id-11296-probe.test.ts @@ -0,0 +1,88 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-kie-11296-probe-")); + +const { handleImageGeneration } = await import( + "../../open-sse/handlers/imageGeneration.ts" +); + +interface CapturedCreate { + url: string; + body: Record; +} + +async function captureCreateTaskModel(publicModel: string): Promise { + const originalFetch = globalThis.fetch; + let captured: CapturedCreate | undefined; + + globalThis.fetch = (async (url: unknown, options: { body?: unknown } = {}) => { + const stringUrl = String(url); + + if (stringUrl === "https://api.kie.ai/api/v1/jobs/createTask") { + captured = { + url: stringUrl, + body: JSON.parse(String(options.body ?? "{}")) as Record, + }; + return new Response(JSON.stringify({ code: 200, data: { taskId: "kie-probe-task-1" } }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + + if (stringUrl.startsWith("https://api.kie.ai/api/v1/jobs/recordInfo")) { + return new Response( + JSON.stringify({ + code: 200, + data: { + state: "success", + resultJson: JSON.stringify({ resultUrls: ["https://example.com/kie-probe.png"] }), + }, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + + throw new Error(`Unexpected URL: ${stringUrl}`); + }) as typeof globalThis.fetch; + + try { + await handleImageGeneration({ + body: { model: publicModel, prompt: "probe prompt", size: "1024x1024", n: 1 }, + credentials: { apiKey: "test-kie-key" }, + log: null, + }); + assert.ok(captured, "expected a createTask request to be captured"); + return String(captured.body.model); + } finally { + globalThis.fetch = originalFetch; + } +} + +test("#11296: GPT Image 2 T2I sends KIE's real upstream id (no namespace prefix)", async () => { + const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-2-text-to-image"); + assert.equal(sentModel, "gpt-image-2-text-to-image"); +}); + +test("#11296: GPT Image 2 I2I sends KIE's real upstream id (no namespace prefix)", async () => { + const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-2-image-to-image"); + assert.equal(sentModel, "gpt-image-2-image-to-image"); +}); + +test("#11296: GPT Image 1.5 T2I sends KIE's real 'gpt-image/' namespace", async () => { + const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-1.5-text-to-image"); + assert.equal(sentModel, "gpt-image/1.5-text-to-image"); +}); + +test("#11296: Seedream 5.0 Lite T2I sends KIE's real id without the '.0'", async () => { + const sentModel = await captureCreateTaskModel("kie/seedream/5.0-lite-text-to-image"); + assert.equal(sentModel, "seedream/5-lite-text-to-image"); +}); + +test("#11296: Flux 2 Pro T2I sends KIE's real 'flux-2/' namespace (dash, not slash)", async () => { + const sentModel = await captureCreateTaskModel("kie/flux/2-pro-text-to-image"); + assert.equal(sentModel, "flux-2/pro-text-to-image"); +}); diff --git a/tests/unit/kie-market-upstream-model-id-11225.test.ts b/tests/unit/kie-market-upstream-model-id-11225.test.ts index 533d0d051f..df81dc414d 100644 --- a/tests/unit/kie-market-upstream-model-id-11225.test.ts +++ b/tests/unit/kie-market-upstream-model-id-11225.test.ts @@ -103,13 +103,21 @@ function resolveLiveKieMarketCatalog() { })); } -test("KIE Market resolver changes exactly the 4 google-imagen ids in the live market catalog", () => { +test("KIE Market resolver changes exactly the documented mismatched ids in the live market catalog", () => { const roundTrips = resolveLiveKieMarketCatalog(); const changed = roundTrips.filter(({ publicModelId, upstreamModelId }) => { return upstreamModelId !== publicModelId; }); assert.deepEqual(changed, [ + { + publicModelId: "seedream/5.0-lite-text-to-image", + upstreamModelId: "seedream/5-lite-text-to-image", + }, + { + publicModelId: "seedream/5.0-lite-image-to-image", + upstreamModelId: "seedream/5-lite-image-to-image", + }, { publicModelId: "google-imagen/nano-banana-2", upstreamModelId: "nano-banana-2", @@ -126,19 +134,71 @@ test("KIE Market resolver changes exactly the 4 google-imagen ids in the live ma publicModelId: "google-imagen/nano-banana-edit", upstreamModelId: "google/nano-banana-edit", }, + { + publicModelId: "flux/2-pro-image-to-image", + upstreamModelId: "flux-2/pro-image-to-image", + }, + { + publicModelId: "flux/2-pro-text-to-image", + upstreamModelId: "flux-2/pro-text-to-image", + }, + { + publicModelId: "flux/2-image-to-image", + upstreamModelId: "flux-2/flex-image-to-image", + }, + { + publicModelId: "flux/2-text-to-image", + upstreamModelId: "flux-2/flex-text-to-image", + }, + { + publicModelId: "gpt/gpt-image-1.5-text-to-image", + upstreamModelId: "gpt-image/1.5-text-to-image", + }, + { + publicModelId: "gpt/gpt-image-1.5-image-to-image", + upstreamModelId: "gpt-image/1.5-image-to-image", + }, + { + publicModelId: "gpt/gpt-image-2-text-to-image", + upstreamModelId: "gpt-image-2-text-to-image", + }, + { + publicModelId: "gpt/gpt-image-2-image-to-image", + upstreamModelId: "gpt-image-2-image-to-image", + }, + { + publicModelId: "wan/2.7-image", + upstreamModelId: "wan/2-7-image", + }, + { + publicModelId: "wan/2.7-image-pro", + upstreamModelId: "wan/2-7-image-pro", + }, ]); }); -const REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS = new Set([ +const REWRITTEN_MARKET_IDS = new Set([ "google-imagen/nano-banana", "google-imagen/nano-banana-2", "google-imagen/nano-banana-pro", "google-imagen/nano-banana-edit", + "gpt/gpt-image-2-text-to-image", + "gpt/gpt-image-2-image-to-image", + "gpt/gpt-image-1.5-text-to-image", + "gpt/gpt-image-1.5-image-to-image", + "seedream/5.0-lite-text-to-image", + "seedream/5.0-lite-image-to-image", + "flux/2-pro-text-to-image", + "flux/2-pro-image-to-image", + "flux/2-text-to-image", + "flux/2-image-to-image", + "wan/2.7-image", + "wan/2.7-image-pro", ]); test("KIE Market resolver preserves every other live market catalog id byte-identically", () => { for (const { publicModelId, upstreamModelId } of resolveLiveKieMarketCatalog()) { - if (!REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS.has(publicModelId)) { + if (!REWRITTEN_MARKET_IDS.has(publicModelId)) { assert.equal( upstreamModelId, publicModelId, @@ -148,8 +208,8 @@ test("KIE Market resolver preserves every other live market catalog id byte-iden } }); -test("KIE Market resolver keeps exactly the explicit google-imagen upstream id mappings (#11296)", () => { - assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 4); +test("KIE Market resolver keeps exactly the explicit upstream id mappings (#11296)", () => { + assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 16); }); test("KIE Market resolver passes an unknown namespaced id through byte-identically", () => { @@ -209,6 +269,78 @@ test("KIE Market createTask sends the KIE upstream id for Nano Banana Edit (#112 ); }); +test("KIE Market createTask sends the unprefixed upstream id for GPT Image 2 T2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/gpt/gpt-image-2-text-to-image"); + + assert.equal(captured.create.body.model, "gpt-image-2-text-to-image"); +}); + +test("KIE Market createTask sends the unprefixed upstream id for GPT Image 2 I2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/gpt/gpt-image-2-image-to-image"); + + assert.equal(captured.create.body.model, "gpt-image-2-image-to-image"); +}); + +test("KIE Market createTask sends the 'gpt-image/' namespace for GPT Image 1.5 T2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/gpt/gpt-image-1.5-text-to-image"); + + assert.equal(captured.create.body.model, "gpt-image/1.5-text-to-image"); +}); + +test("KIE Market createTask sends the 'gpt-image/' namespace for GPT Image 1.5 I2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/gpt/gpt-image-1.5-image-to-image"); + + assert.equal(captured.create.body.model, "gpt-image/1.5-image-to-image"); +}); + +test("KIE Market createTask drops the '.0' for Seedream 5.0 Lite T2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/seedream/5.0-lite-text-to-image"); + + assert.equal(captured.create.body.model, "seedream/5-lite-text-to-image"); +}); + +test("KIE Market createTask drops the '.0' for Seedream 5.0 Lite I2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/seedream/5.0-lite-image-to-image"); + + assert.equal(captured.create.body.model, "seedream/5-lite-image-to-image"); +}); + +test("KIE Market createTask sends the 'flux-2/' namespace for Flux 2 Pro T2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/flux/2-pro-text-to-image"); + + assert.equal(captured.create.body.model, "flux-2/pro-text-to-image"); +}); + +test("KIE Market createTask sends the 'flux-2/' namespace for Flux 2 Pro I2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/flux/2-pro-image-to-image"); + + assert.equal(captured.create.body.model, "flux-2/pro-image-to-image"); +}); + +test("KIE Market createTask sends the 'flux-2/flex-' name for Flux 2 T2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/flux/2-text-to-image"); + + assert.equal(captured.create.body.model, "flux-2/flex-text-to-image"); +}); + +test("KIE Market createTask sends the 'flux-2/flex-' name for Flux 2 I2I (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/flux/2-image-to-image"); + + assert.equal(captured.create.body.model, "flux-2/flex-image-to-image"); +}); + +test("KIE Market createTask sends the dash-separated id for Wan 2.7 Image (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/wan/2.7-image"); + + assert.equal(captured.create.body.model, "wan/2-7-image"); +}); + +test("KIE Market createTask sends the dash-separated id for Wan 2.7 Image Pro (#11296)", async () => { + const captured = await runKieMarketGeneration("kie/wan/2.7-image-pro"); + + assert.equal(captured.create.body.model, "wan/2-7-image-pro"); +}); + test("KIE Market createTask leaves genuinely namespaced upstream ids untouched (#11225 control)", async () => { const captured = await runKieMarketGeneration("kie/seedream/4.5-text-to-image");