mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: show custom vision models in LLM selector and model list
Expose user-added imageToText custom models as vision-capable chat
models in the default LLM selector and /v1/models, map custom service
kinds to runtime capabilities, and keep typed filtering for
/v1/models/{kind}.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
decolua
co-authored by
Cursor
parent
047fdc8960
commit
5e5e78d3e8
@@ -51,6 +51,21 @@ export const DEFAULT_CAPABILITIES = {
|
|||||||
maxOutput: 64000,
|
maxOutput: 64000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// User-added model metadata can carry dashboard service kinds instead of the
|
||||||
|
// runtime capability names used here. Map those typed model kinds into input /
|
||||||
|
// output capabilities so custom vision models are not treated as text-only.
|
||||||
|
const SERVICE_KIND_CAPABILITIES = {
|
||||||
|
imageToText: { vision: true },
|
||||||
|
image: { imageOutput: true },
|
||||||
|
stt: { audioInput: true },
|
||||||
|
tts: { audioOutput: true },
|
||||||
|
embedding: { tools: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function capabilitiesFromServiceKind(kind) {
|
||||||
|
return SERVICE_KIND_CAPABILITIES[kind] || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canonical exact-id overrides — used for exceptions that patterns would
|
* Canonical exact-id overrides — used for exceptions that patterns would
|
||||||
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
* otherwise mis-match. Only declare deltas vs DEFAULT.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getProviderConnections, getCombos, getCustomModels, getModelAliases } f
|
|||||||
import { getDisabledModels } from "@/lib/disabledModelsDb";
|
import { getDisabledModels } from "@/lib/disabledModelsDb";
|
||||||
import { resolveKiroModels } from "open-sse/services/kiroModels.js";
|
import { resolveKiroModels } from "open-sse/services/kiroModels.js";
|
||||||
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
|
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
|
||||||
|
import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js";
|
||||||
|
|
||||||
// Per-provider live model resolvers. Each receives a connection record and
|
// Per-provider live model resolvers. Each receives a connection record and
|
||||||
// returns { models: [{ id, name? }, ...] } | null on failure.
|
// returns { models: [{ id, name? }, ...] } | null on failure.
|
||||||
@@ -314,13 +315,22 @@ export async function buildModelsList(kindFilter) {
|
|||||||
})
|
})
|
||||||
.filter((modelId) => typeof modelId === "string" && modelId.trim() !== "");
|
.filter((modelId) => typeof modelId === "string" && modelId.trim() !== "");
|
||||||
|
|
||||||
|
const customModelKindById = new Map();
|
||||||
const customModelIds = customModels
|
const customModelIds = customModels
|
||||||
.filter((m) => {
|
.filter((m) => {
|
||||||
if (!m?.id || (getModelKind(m) && getModelKind(m) !== "llm")) return false;
|
if (!m?.id) return false;
|
||||||
|
const kind = getModelKind(m) || LLM_KIND;
|
||||||
|
// imageToText custom models are vision-capable chat models: expose them
|
||||||
|
// both in the default LLM list and in /v1/models/image-to-text.
|
||||||
|
if (!kindFilter.includes(kind) && !(kind === "imageToText" && kindFilter.includes(LLM_KIND))) return false;
|
||||||
const alias = m.providerAlias;
|
const alias = m.providerAlias;
|
||||||
return alias === staticAlias || alias === outputAlias || alias === providerId;
|
return alias === staticAlias || alias === outputAlias || alias === providerId;
|
||||||
})
|
})
|
||||||
.map((m) => String(m.id).trim())
|
.map((m) => {
|
||||||
|
const modelId = String(m.id).trim();
|
||||||
|
if (modelId) customModelKindById.set(modelId, getModelKind(m) || LLM_KIND);
|
||||||
|
return modelId;
|
||||||
|
})
|
||||||
.filter((modelId) => modelId !== "");
|
.filter((modelId) => modelId !== "");
|
||||||
|
|
||||||
const aliasModelIds = Object.values(modelAliases || {})
|
const aliasModelIds = Object.values(modelAliases || {})
|
||||||
@@ -349,16 +359,22 @@ export async function buildModelsList(kindFilter) {
|
|||||||
const mergedModelIds = Array.from(new Set([...modelIds, ...customModelIds, ...aliasModelIds]));
|
const mergedModelIds = Array.from(new Set([...modelIds, ...customModelIds, ...aliasModelIds]));
|
||||||
|
|
||||||
for (const modelId of mergedModelIds) {
|
for (const modelId of mergedModelIds) {
|
||||||
// Resolve kind: prefer static metadata, otherwise infer from ID heuristics
|
// Resolve kind: prefer static/custom metadata, otherwise infer from ID heuristics
|
||||||
const kind = staticModelKindById.get(modelId) || inferKindFromUnknownModelId(modelId);
|
const customKind = customModelKindById.get(modelId);
|
||||||
if (!kindFilter.includes(kind)) continue;
|
const kind = staticModelKindById.get(modelId) || customKind || inferKindFromUnknownModelId(modelId);
|
||||||
|
// imageToText custom models stay in the LLM list (vision-capable chat models)
|
||||||
|
const allowAsLlm = kind === "imageToText" && kindFilter.includes(LLM_KIND);
|
||||||
|
if (!kindFilter.includes(kind) && !allowAsLlm) continue;
|
||||||
if (isDisabled(outputAlias, modelId) || isDisabled(staticAlias, modelId)) continue;
|
if (isDisabled(outputAlias, modelId) || isDisabled(staticAlias, modelId)) continue;
|
||||||
|
|
||||||
models.push({
|
const model = {
|
||||||
id: `${outputAlias}/${modelId}`,
|
id: `${outputAlias}/${modelId}`,
|
||||||
object: "model",
|
object: "model",
|
||||||
owned_by: outputAlias,
|
owned_by: outputAlias,
|
||||||
});
|
};
|
||||||
|
const caps = capabilitiesFromServiceKind(customKind);
|
||||||
|
if (caps) model.capabilities = caps;
|
||||||
|
models.push(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web search/fetch — provider IS the model, expose as {alias}/search and/or {alias}/fetch with explicit kind
|
// Web search/fetch — provider IS the model, expose as {alias}/search and/or {alias}/fetch with explicit kind
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ export default function ModelSelectModal({
|
|||||||
|
|
||||||
// Filter a models[] array by kindFilter (keep only matching kind)
|
// Filter a models[] array by kindFilter (keep only matching kind)
|
||||||
const filterByKind = (models) => {
|
const filterByKind = (models) => {
|
||||||
if (!kindFilter) return models.filter((m) => m.isPlaceholder || !getModelKind(m) || getModelKind(m) === "llm");
|
// No kindFilter means the LLM selector. Keep custom models visible because
|
||||||
|
// user-added models may have typed capabilities (for example imageToText)
|
||||||
|
// while still being valid chat/combo targets.
|
||||||
|
if (!kindFilter) return models.filter((m) => m.isPlaceholder || m.isCustom || !getModelKind(m) || getModelKind(m) === "llm");
|
||||||
if (!TYPED_KINDS.has(kindFilter)) return models;
|
if (!TYPED_KINDS.has(kindFilter)) return models;
|
||||||
return models.filter((m) => m.isPlaceholder || getModelKind(m) === kindFilter);
|
return models.filter((m) => m.isPlaceholder || getModelKind(m) === kindFilter);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { capabilitiesFromServiceKind } from "../../open-sse/providers/capabilities.js";
|
||||||
|
|
||||||
|
describe("capabilitiesFromServiceKind", () => {
|
||||||
|
it("maps imageToText custom models to vision-capable runtime models", () => {
|
||||||
|
expect(capabilitiesFromServiceKind("imageToText")).toMatchObject({ vision: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps media output/input custom model kinds to runtime capabilities", () => {
|
||||||
|
expect(capabilitiesFromServiceKind("image")).toMatchObject({ imageOutput: true });
|
||||||
|
expect(capabilitiesFromServiceKind("stt")).toMatchObject({ audioInput: true });
|
||||||
|
expect(capabilitiesFromServiceKind("tts")).toMatchObject({ audioOutput: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user