fix(models): list compatible provider models in /v1/models
Replace the overly-broad UPSTREAM_CONNECTION_RE regex (which matched all provider IDs with UUID suffixes) with an x-9r-internal-models-fetch header to detect cross-instance recursive /models fetches. fetchCompatibleModelIds now sends the header when fetching upstream /models; the GET handler detects it and skips dynamic fetching, breaking the recursion loop while letting compatible providers (MLX, Ollama, vLLM) list their models. Fixes #2626.
This commit is contained in:
@@ -79,8 +79,9 @@ const parseOpenAIStyleModels = (data) => {
|
||||
return data?.data || data?.models || data?.results || [];
|
||||
};
|
||||
|
||||
// Matches provider IDs that are upstream/cross-instance connections (contain a UUID suffix)
|
||||
const UPSTREAM_CONNECTION_RE = /[-_][0-9a-f]{8,}$/i;
|
||||
// Header sent by fetchCompatibleModelIds to detect cross-instance /models fetches
|
||||
// and break recursive loops between 9router instances connected to each other.
|
||||
const INTERNAL_MODELS_FETCH_HEADER = "x-9r-internal-models-fetch";
|
||||
|
||||
// LLM kind sentinel — combos/models with no explicit kind default to LLM
|
||||
const LLM_KIND = "llm";
|
||||
@@ -145,7 +146,7 @@ async function fetchCompatibleModelIds(connection) {
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
headers: { ...headers, [INTERNAL_MODELS_FETCH_HEADER]: "1" },
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -189,7 +190,11 @@ function comboMatchesKinds(combo, kindFilter) {
|
||||
* Build OpenAI-format models list filtered by service kinds.
|
||||
* @param {string[]} kindFilter - List of service kinds to include (e.g. ["llm"], ["webSearch","webFetch"]).
|
||||
*/
|
||||
export async function buildModelsList(kindFilter) {
|
||||
export async function buildModelsList(kindFilter, options = {}) {
|
||||
// When this header is present, the /v1/models request came from another
|
||||
// 9router instance's fetchCompatibleModelIds — skip dynamic fetch to break
|
||||
// cross-instance recursive loops.
|
||||
const skipDynamicFetch = options.skipDynamicFetch === true;
|
||||
let connections = [];
|
||||
try {
|
||||
connections = await getProviderConnections();
|
||||
@@ -319,7 +324,7 @@ export async function buildModelsList(kindFilter) {
|
||||
)
|
||||
: providerModels.map((model) => model.id);
|
||||
|
||||
if (isCompatibleProvider && rawModelIds.length === 0 && !UPSTREAM_CONNECTION_RE.test(providerId)) {
|
||||
if (isCompatibleProvider && rawModelIds.length === 0 && !skipDynamicFetch) {
|
||||
rawModelIds = await fetchCompatibleModelIds(conn);
|
||||
}
|
||||
|
||||
@@ -475,9 +480,11 @@ export async function OPTIONS() {
|
||||
* GET /v1/models - OpenAI compatible models list (LLM/chat models only by default).
|
||||
* For other capabilities use /v1/models/{kind} (image, tts, stt, embedding, image-to-text, web).
|
||||
*/
|
||||
export async function GET() {
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const data = await buildModelsList([LLM_KIND]);
|
||||
// Detect cross-instance recursive /models fetch (another 9router fetching our /models)
|
||||
const skipDynamicFetch = request?.headers?.get(INTERNAL_MODELS_FETCH_HEADER) === "1";
|
||||
const data = await buildModelsList([LLM_KIND], { skipDynamicFetch });
|
||||
return Response.json({ object: "list", data }, {
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user