mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
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:
@@ -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
|
||||
@@ -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 =
|
||||
|
||||
106
tests/unit/openrouter-models-baseurl-override.test.ts
Normal file
106
tests/unit/openrouter-models-baseurl-override.test.ts
Normal 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)}`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user