mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* fix(api): allow text-to-image on dual-modality models + revive HuggingFace image host
Two image-generation regressions surfaced while testing /v1/images/generations:
1. Dual-modality models (inputModalities ["text","image"]) were rejected with
"Image input is required" because the gate treated any "image" modality as
mandatory. That blocked pure text-to-image on 41 models (Together x10,
Stability x10, LMArena x15, NVIDIA x3, BFL x2, NanoGPT x1). Only edit-only
models (modalities ["image"] with no "text") should require an image input;
extract modalitiesRequireImageInput() and gate on that.
2. The HuggingFace image provider pointed at api-inference.huggingface.co, which
HF retired (DNS-dead -> "fetch failed" 502). Route through
router.huggingface.co/hf-inference/models, matching the chat provider which
already migrated.
Regression guard: tests/unit/image-text-to-image-modality.test.ts (fails on base
-- the helper did not exist and the baseUrl was the retired host).
* fix(api): keep Stability edit/control/upscale endpoints image-required
modalitiesRequireImageInput() correctly stopped gating dual-modality
(text+image) generation models on an image input, fixing pure
text-to-image for 41 models. But 10 of those dual-modality entries are
Stability AI's dedicated /v2beta/stable-image/{edit,control,upscale}/*
endpoints (inpaint, outpaint, search-and-replace, search-and-recolor,
replace-background-and-relight, creative, sketch, structure, style,
style-transfer) — they accept a text prompt too, but mechanically
require an input image upstream. The blanket modality-based inference
silently dropped OmniRoute's client-side gate for exactly those 10,
trading a clean 400 for a confusing upstream Stability error.
Add an explicit `imageRequired` override on the registry entry, decided
by the model's actual endpoint rather than inferred from its listed
modalities, and combine it with modalitiesRequireImageInput() at the
route gate: `imageModelEntry?.imageRequired || modalitiesRequireImageInput(...)`.
Extracted the Stability AI model list into
providers/registry/stability-ai/imageModels.ts (mirroring the existing
kie/segmind pattern) — imageRegistry.ts sits right at the 800-line
file-size cap and the extra flags would have pushed it over.
Extended tests/unit/image-text-to-image-modality.test.ts: the previous
"no dual-modality model is gated as image-required" assertion was
exactly the bug (it would have passed even with the regression); new
assertions cover the 10 Stability edit/control/upscale models by id
(still require an image) alongside the true dual-modality generation
models (BFL Kontext, NVIDIA, NanoGPT — still accept text-only).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
146 lines
6.1 KiB
TypeScript
146 lines
6.1 KiB
TypeScript
/**
|
|
* Two image-generation regressions:
|
|
*
|
|
* 1. Dual-modality text-to-image models (inputModalities === ["text","image"]) were
|
|
* rejected on /v1/images/generations with "Image input is required" because the
|
|
* gate treated any "image" modality as mandatory. 41 models (Together, Stability,
|
|
* LMArena, NVIDIA, BFL, NanoGPT) could not do pure text-to-image as a result.
|
|
*
|
|
* Follow-up carve-out: 10 of those Stability AI models (inpaint, outpaint,
|
|
* search-and-replace, search-and-recolor, replace-background-and-relight,
|
|
* creative, sketch, structure, style, style-transfer) are Stability's dedicated
|
|
* /v2beta/stable-image/{edit,control,upscale}/* endpoints (STABILITY_EDIT_ENDPOINTS
|
|
* in open-sse/handlers/imageGeneration.ts) — they accept a text prompt too, but
|
|
* mechanically REQUIRE an input image. modalitiesRequireImageInput() alone can't
|
|
* distinguish them from the flexible dual-modality generation models (BFL Kontext,
|
|
* Together, NVIDIA, LMArena, NanoGPT), so the registry carries an explicit
|
|
* `imageRequired: true` flag on exactly those 10 entries, and the route gate
|
|
* (src/app/api/v1/images/generations/route.ts) combines both signals:
|
|
* `imageModelEntry?.imageRequired || modalitiesRequireImageInput(inputModalities)`.
|
|
*
|
|
* 2. The HuggingFace image provider pointed at the retired api-inference.huggingface.co
|
|
* host (DNS-dead → "fetch failed" 502). Text-to-image now routes through
|
|
* router.huggingface.co with the hf-inference provider pinned in the path.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
IMAGE_PROVIDERS,
|
|
getImageModelEntry,
|
|
modalitiesRequireImageInput,
|
|
} from "../../open-sse/config/imageRegistry.ts";
|
|
|
|
// The 10 Stability AI edit/control/upscale endpoints that mechanically require an
|
|
// input image even though they also accept a text prompt — STABILITY_EDIT_ENDPOINTS
|
|
// in open-sse/handlers/imageGeneration.ts, minus the image-only entries (erase,
|
|
// remove-background, fast, conservative) which were never affected by this bug
|
|
// because their inputModalities never included "text" in the first place.
|
|
const STABILITY_IMAGE_REQUIRED_MODELS = [
|
|
"inpaint",
|
|
"outpaint",
|
|
"search-and-replace",
|
|
"search-and-recolor",
|
|
"replace-background-and-relight",
|
|
"creative",
|
|
"sketch",
|
|
"structure",
|
|
"style",
|
|
"style-transfer",
|
|
];
|
|
|
|
// Mirrors the combined gate in src/app/api/v1/images/generations/route.ts.
|
|
function effectiveRequiresImageInput(entry, inputModalities) {
|
|
return Boolean(entry?.imageRequired) || modalitiesRequireImageInput(inputModalities);
|
|
}
|
|
|
|
test("modalitiesRequireImageInput: only edit-only models require an image input", () => {
|
|
// Edit-only → image is mandatory.
|
|
assert.equal(modalitiesRequireImageInput(["image"]), true);
|
|
// Dual text-to-image + image-to-image → image optional (the bug: these were blocked).
|
|
assert.equal(modalitiesRequireImageInput(["text", "image"]), false);
|
|
// Text-only → never requires an image.
|
|
assert.equal(modalitiesRequireImageInput(["text"]), false);
|
|
// Defensive: undefined/non-array defaults to text-only behavior.
|
|
assert.equal(modalitiesRequireImageInput(undefined), false);
|
|
assert.equal(modalitiesRequireImageInput(null), false);
|
|
});
|
|
|
|
test("dual-modality (text+image) registry models allow pure text-to-image, EXCEPT the Stability edit/control/upscale set flagged imageRequired", () => {
|
|
const wronglyBlocked = [];
|
|
const wronglyAllowed = [];
|
|
for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) {
|
|
for (const model of config.models || []) {
|
|
const im = model.inputModalities || ["text"];
|
|
if (!im.includes("text") || !im.includes("image")) continue;
|
|
const effective = effectiveRequiresImageInput(model, im);
|
|
if (model.imageRequired) {
|
|
if (!effective) wronglyAllowed.push(`${providerId}/${model.id}`);
|
|
} else if (effective) {
|
|
wronglyBlocked.push(`${providerId}/${model.id}`);
|
|
}
|
|
}
|
|
}
|
|
assert.deepEqual(
|
|
wronglyBlocked,
|
|
[],
|
|
"dual text+image models without imageRequired must allow pure text-to-image"
|
|
);
|
|
assert.deepEqual(
|
|
wronglyAllowed,
|
|
[],
|
|
"dual text+image models flagged imageRequired must still gate on image input"
|
|
);
|
|
});
|
|
|
|
test("stability-ai edit/control/upscale models still REQUIRE an image despite accepting a text prompt", () => {
|
|
for (const modelId of STABILITY_IMAGE_REQUIRED_MODELS) {
|
|
const entry = getImageModelEntry(`stability-ai/${modelId}`);
|
|
assert.ok(entry, `stability-ai/${modelId} must resolve to a registry entry`);
|
|
assert.equal(
|
|
entry.imageRequired,
|
|
true,
|
|
`stability-ai/${modelId} must be flagged imageRequired`
|
|
);
|
|
assert.equal(
|
|
effectiveRequiresImageInput(entry, entry.inputModalities),
|
|
true,
|
|
`stability-ai/${modelId} must still gate on image input (mechanically requires one upstream)`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("true dual-modality generation models (BFL Kontext, NVIDIA, NanoGPT) accept text-only", () => {
|
|
const dualModalitySamples = [
|
|
"black-forest-labs/flux-kontext-pro",
|
|
"nvidia/black-forest-labs/flux.1-dev",
|
|
"nanogpt/qwen-image",
|
|
];
|
|
for (const modelStr of dualModalitySamples) {
|
|
const entry = getImageModelEntry(modelStr);
|
|
assert.ok(entry, `${modelStr} must resolve to a registry entry`);
|
|
assert.ok(!entry.imageRequired, `${modelStr} must not be flagged imageRequired`);
|
|
assert.equal(
|
|
effectiveRequiresImageInput(entry, entry.inputModalities),
|
|
false,
|
|
`${modelStr} must accept pure text-to-image`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("HuggingFace image provider uses the live router host, not the retired api-inference host", () => {
|
|
const hf = IMAGE_PROVIDERS.huggingface;
|
|
assert.ok(hf, "huggingface image provider must exist");
|
|
assert.equal(hf.baseUrl, "https://router.huggingface.co/hf-inference/models");
|
|
assert.ok(
|
|
!hf.baseUrl.includes("api-inference.huggingface.co"),
|
|
"must not use the DNS-dead api-inference.huggingface.co host"
|
|
);
|
|
// The handler builds `${baseUrl}/${model}` — assert the resulting URL is the router form.
|
|
const model = hf.models[0].id;
|
|
assert.equal(
|
|
`${hf.baseUrl}/${model}`,
|
|
`https://router.huggingface.co/hf-inference/models/${model}`
|
|
);
|
|
});
|