fix(providers): honor the base URL override in OpenRouter model discovery (#14001)

* fix(providers): honor the base URL override in OpenRouter model discovery

Model discovery for the built-in `openrouter` provider resolved its catalog
URL from PROVIDER_MODELS_CONFIG, which is pinned to the global
`https://openrouter.ai/api/v1/models`. The per-connection base-URL override
(`providerSpecificData.baseUrl`, set via "Advanced -> override base URL") was
never consulted on the discovery path, while the inference path has honored it
since #6147 (open-sse/executors/base.ts `resolveBaseUrl`).

A connection pointed at a different OpenRouter region therefore kept importing
the global catalog: the per-connection model list, and the auto-sync that
maintains it, advertised model ids the configured endpoint cannot serve. Those
ids only failed later, at inference time, so a region/catalog mismatch surfaced
as what looked like a provider outage.

The two catalogs genuinely differ — the global endpoint advertises ~444 model
ids, the EU in-region endpoint ~58 (a strict subset) — so discovery and
inference disagreed with no signal exposing it.

Discovery now prefers the override for this provider, reusing the existing
`addModelsSuffix()` normalization (drops a trailing chat/responses/messages
path, appends /models, leaves an existing /models untouched). Mirrors the
`openai` override handling added for the same class of bug in #5899. When no
override is set the built-in global catalog is still used.

Tests: tests/unit/openrouter-models-baseurl-override.test.ts covers both the
override and the unchanged default.

* chore(changelog): add fragment for #14001

---------

Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro>
This commit is contained in:
Tiangao
2026-09-18 16:34:47 +02:00
committed by GitHub
parent 6f55a8c44f
commit 95cb992c32
3 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1 @@
- **fix(providers):** OpenRouter model discovery honors the per-connection base URL override instead of always importing the global catalog, so a connection pointed at a regional endpoint (e.g. the EU in-region host) no longer advertises model ids that endpoint cannot serve ([#14001](https://github.com/diegosouzapw/OmniRoute/pull/14001)) — thanks @tiangao88

View File

@@ -12,6 +12,7 @@ import { resolveAlibabaProviderModelsUrl } from "@/shared/constants/alibabaProvi
import { getStaticModelsForProvider } from "@/lib/providers/staticModels";
import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability";
import { mergeModelsWithCustomPrecedence } from "@/lib/providers/modelMetadataPrecedence";
import { addModelsSuffix } from "@/lib/providers/validation/urlHelpers";
import { getCachedProviderConnectionById } from "@/lib/db/readCache";
import { resolveProxyForProvider } from "@/lib/db/proxies";
import {
@@ -2168,6 +2169,23 @@ export async function GET(
url = `${base}/v1/models`;
}
}
// OpenRouter is pinned by PROVIDER_MODELS_CONFIG to the *global* catalog
// (https://openrouter.ai/api/v1/models), so it never consulted the
// per-connection base-URL override. A connection pointed at a different
// OpenRouter region — e.g. the EU in-region endpoint
// (https://eu.openrouter.ai/api/v1) — therefore kept importing the global
// catalog and advertised models that endpoint cannot serve. Those ids then
// 404 at inference time even though the per-connection model list, and the
// auto-sync that maintains it, look healthy. Mirrors the `openai` override
// handling above (#5899). addModelsSuffix() reuses the shared normalization:
// it drops a trailing chat/responses/messages path, appends /models, and
// leaves an already-/models URL untouched.
if (provider === "openrouter") {
const customBaseUrl = getProviderBaseUrl(connection.providerSpecificData);
if (customBaseUrl) {
url = addModelsSuffix(customBaseUrl) || url;
}
}
if (provider === "cloudflare-ai") {
const pData = asRecord(connection.providerSpecificData);
const accountId =

View File

@@ -0,0 +1,106 @@
// Model discovery for the built-in `openrouter` provider was pinned to the
// GLOBAL catalog URL in PROVIDER_MODELS_CONFIG
// ("https://openrouter.ai/api/v1/models"), so the per-connection base-URL
// override ("Advanced → override base URL", stored as
// providerSpecificData.baseUrl) was silently ignored on the DISCOVERY path —
// while the INFERENCE path honored it all along
// (open-sse/executors/base.ts `resolveBaseUrl`: "Operator's manual override
// always wins (#6147)").
//
// Consequence: a connection pointed at a different OpenRouter region (for
// example the EU in-region endpoint https://eu.openrouter.ai/api/v1) kept
// importing the global catalog, so the per-connection model list — and the
// auto-sync that maintains it — advertised models that the configured endpoint
// cannot serve. Those ids only failed later, at inference time, which made the
// misconfiguration look like a provider outage rather than a catalog mismatch.
//
// The two OpenRouter catalogs genuinely differ: the global endpoint advertises
// ~444 model ids, the EU in-region endpoint ~58 (a strict subset).
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-openrouter-baseurl-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
const originalFetch = globalThis.fetch;
const EU_BASE_URL = "https://eu.openrouter.ai/api/v1";
const GLOBAL_CATALOG_URL = "https://openrouter.ai/api/v1/models";
async function resetStorage() {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
async function seedOpenRouterConnection(providerSpecificData: Record<string, unknown>) {
return providersDb.createProviderConnection({
provider: "openrouter",
authType: "apikey",
name: `openrouter-${Math.random().toString(16).slice(2, 8)}`,
apiKey: "[REDACTED:auth_header]",
isActive: true,
testStatus: "active",
providerSpecificData,
});
}
async function discover(connectionId: string) {
const seenUrls: string[] = [];
globalThis.fetch = async (url) => {
seenUrls.push(String(url));
return Response.json({ data: [{ id: "openai/gpt-5.6-luna", name: "GPT-5.6 Luna" }] });
};
const response = await providerModelsRoute.GET(
new Request(`http://localhost/api/providers/${connectionId}/models?refresh=true`),
{ params: { id: connectionId } }
);
return { response, seenUrls };
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("openrouter model discovery honors the per-connection base URL override", async () => {
const connection = await seedOpenRouterConnection({ baseUrl: EU_BASE_URL });
const { response, seenUrls } = await discover(connection.id);
assert.equal(response.status, 200);
assert.ok(
seenUrls.some((url) => url.startsWith(`${EU_BASE_URL}/models`)),
`expected discovery against the overridden base URL, got: ${JSON.stringify(seenUrls)}`
);
assert.ok(
!seenUrls.some((url) => url.startsWith(GLOBAL_CATALOG_URL)),
`expected the hardcoded global catalog NOT to be used, got: ${JSON.stringify(seenUrls)}`
);
});
test("openrouter discovery still uses the built-in global catalog when no override is set", async () => {
const connection = await seedOpenRouterConnection({});
const { response, seenUrls } = await discover(connection.id);
assert.equal(response.status, 200);
assert.ok(
seenUrls.some((url) => url.startsWith(GLOBAL_CATALOG_URL)),
`expected the built-in global catalog, got: ${JSON.stringify(seenUrls)}`
);
});