mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
detectVisionInput() only recognized supportsVision, architecture.input_modalities, top-level input_modalities, and architecture/modality string shapes. Lemonade Server's GET /v1/models exposes capabilities only through a labels[] string array (e.g. ["chat", "vision", "reasoning", "tool-calling"]), so a vision-labelled Lemonade model imported with supportsVision unset and was advertised as text-only. Add a fifth branch that does a case-insensitive, trimmed EXACT membership test for "vision" in record.labels[] (not a substring match, per the prior false-positive lesson with bare gemma id-fragment matching). Purely additive - all four existing shapes stay byte-identical, proven by a new regression test that exercises the architecture.modality path unchanged.
This commit is contained in:
committed by
GitHub
parent
9c9ad6bbde
commit
3b535968c4
1
changelog.d/fixes/13918-lemonade-vision-labels.md
Normal file
1
changelog.d/fixes/13918-lemonade-vision-labels.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): `detectVisionInput` now recognizes Lemonade Server's `labels[]` vision capability, so Lemonade vision models import with `supportsVision` set instead of being treated as text-only (#13918)
|
||||
@@ -41,12 +41,26 @@ function modalitiesIncludeImage(value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// #13918: Lemonade Server's GET /v1/models exposes capabilities only through a
|
||||
// `labels[]` string array (e.g. ["chat", "vision", "reasoning", "tool-calling"]) —
|
||||
// it has none of the modality/architecture fields the other shapes below read.
|
||||
// See https://lemonade-server.ai/docs/api/openai/. Exact (case-insensitive,
|
||||
// trimmed) membership test only — not a substring match, per the earlier
|
||||
// false-positive lesson with bare `gemma` id-fragment matching.
|
||||
function labelsIncludeVision(value: unknown): boolean {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.some((entry) => toNonEmptyString(entry)?.toLowerCase() === "vision")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* #4264: detect image-input (vision) capability from a discovered model record.
|
||||
* Handles the common upstream shapes: an explicit `supportsVision` flag, the
|
||||
* OpenRouter `architecture.input_modalities` array and string `architecture.modality`
|
||||
* ("text+image->text" — the input side is everything before "->"), and a top-level
|
||||
* `input_modalities` array. Returns false when the upstream exposes no modality info.
|
||||
* ("text+image->text" — the input side is everything before "->"), a top-level
|
||||
* `input_modalities` array, and (#13918) Lemonade Server's `labels[]` array.
|
||||
* Returns false when the upstream exposes no modality info.
|
||||
*/
|
||||
export function detectVisionInput(record: JsonRecord): boolean {
|
||||
if (record.supportsVision === true) return true;
|
||||
@@ -60,6 +74,9 @@ export function detectVisionInput(record: JsonRecord): boolean {
|
||||
const [inputPart] = modality.toLowerCase().split("->");
|
||||
if ((inputPart || "").includes("image")) return true;
|
||||
}
|
||||
|
||||
if (labelsIncludeVision(record.labels)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
86
tests/unit/lemonade-vision-labels-13918.test.ts
Normal file
86
tests/unit/lemonade-vision-labels-13918.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { detectVisionInput } from "@/lib/providerModels/modelDiscovery";
|
||||
|
||||
// #13918: Lemonade Server's GET /v1/models exposes capabilities only through a
|
||||
// `labels[]` string array (chat, vision, reasoning, tool-calling, ...) -- it has
|
||||
// no `architecture.input_modalities` / `input_modalities` / `modality` field, so
|
||||
// detectVisionInput() never recognizes a vision-labelled Lemonade model.
|
||||
|
||||
test("detectVisionInput recognizes a Lemonade record whose labels[] includes vision", () => {
|
||||
const lemonadeRecord = {
|
||||
id: "Gemma-4-26B-A4B",
|
||||
object: "model",
|
||||
owned_by: "lemonade",
|
||||
checkpoint: "amd/gemma-4-26b-a4b",
|
||||
recipe: "llamacpp",
|
||||
size: 26000000000,
|
||||
max_context_window: 262144,
|
||||
context_length: 262144,
|
||||
downloaded: true,
|
||||
labels: ["chat", "vision", "reasoning", "tool-calling"],
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
detectVisionInput(lemonadeRecord),
|
||||
true,
|
||||
"expected a Lemonade record labelled `vision` to be detected as vision-capable"
|
||||
);
|
||||
});
|
||||
|
||||
test("detectVisionInput stays false for a Lemonade record without the vision label", () => {
|
||||
const textOnly = {
|
||||
id: "Gemma-4-26B-A4B-text",
|
||||
owned_by: "lemonade",
|
||||
labels: ["chat", "tool-calling"],
|
||||
};
|
||||
|
||||
assert.equal(detectVisionInput(textOnly), false);
|
||||
});
|
||||
|
||||
test("detectVisionInput is case-insensitive and trims whitespace in labels[]", () => {
|
||||
const mixedCase = {
|
||||
id: "some-model",
|
||||
owned_by: "lemonade",
|
||||
labels: ["chat", " Vision "],
|
||||
};
|
||||
|
||||
assert.equal(detectVisionInput(mixedCase), true);
|
||||
});
|
||||
|
||||
test("detectVisionInput does not substring-match labels[] entries", () => {
|
||||
const notVision = {
|
||||
id: "some-model",
|
||||
owned_by: "lemonade",
|
||||
labels: ["chat", "revision-control"],
|
||||
};
|
||||
|
||||
assert.equal(detectVisionInput(notVision), false);
|
||||
});
|
||||
|
||||
test("detectVisionInput ignores non-array labels without throwing", () => {
|
||||
assert.doesNotThrow(() => detectVisionInput({ id: "x", labels: "vision" as unknown }));
|
||||
assert.equal(detectVisionInput({ id: "x", labels: "vision" as unknown }), false);
|
||||
assert.doesNotThrow(() => detectVisionInput({ id: "x", labels: undefined }));
|
||||
assert.equal(detectVisionInput({ id: "x", labels: {} as unknown }), false);
|
||||
});
|
||||
|
||||
test("detectVisionInput leaves non-Lemonade shapes byte-identical (architecture.modality path)", () => {
|
||||
const openRouterRecord = {
|
||||
id: "some/vision-model",
|
||||
architecture: {
|
||||
modality: "text+image->text",
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(detectVisionInput(openRouterRecord), true);
|
||||
|
||||
const openRouterTextOnly = {
|
||||
id: "some/text-model",
|
||||
architecture: {
|
||||
modality: "text->text",
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(detectVisionInput(openRouterTextOnly), false);
|
||||
});
|
||||
Reference in New Issue
Block a user