mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
fix: match compatible-provider models owned by public prefix (#13831)
* fix: match compatible-provider models owned by public prefix Resolves #13829 The provider-scoped /v1/providers/{provider}/models route filters unified-catalog rows by internal provider ID. For compatible provider nodes, the catalog emits the configured public prefix in owned_by, so all valid models were dropped and the endpoint returned an empty list. Resolve the compatible node's prefix and accept it alongside the internal ID when filtering and when stripping the prefix from returned model ids. * chore(quality): satisfy the format and lint-suppression gates for #13829 Two gate-only touch-ups on top of the fix, no behaviour change: - prettier --check rejected tests/unit/provider-models-v1-route.test.ts over a double blank line before a test block. - typing the map callback removed the file's only `any`, which left the frozen entry in config/quality/eslint-suppressions.json unused; the lint gate fails on a stale suppression, so it is pruned. Both were found by running the gates locally, because this fork PR's workflow runs are still awaiting maintainer approval and only the semgrep check had run. Co-authored-by: sahildaswani <sahildaswani@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: sahildaswani <sahildaswani@users.noreply.github.com>
This commit is contained in:
@@ -4435,11 +4435,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"tests/unit/provider-models-v1-route.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"tests/unit/provider-node-icon-url.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 6
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog";
|
||||
import { getProviderNodeById } from "@/lib/db/providers/nodes";
|
||||
import { getServiceModels } from "@/lib/db/serviceModels";
|
||||
import { isServiceBackendPluginId } from "@/lib/services/serviceBackends";
|
||||
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
@@ -40,6 +41,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ prov
|
||||
const providerEntry = getRegistryEntry(rawProvider);
|
||||
let providerId = rawProvider;
|
||||
let providerAlias = rawProvider;
|
||||
let compatiblePrefix: string | null = null;
|
||||
|
||||
if (providerEntry) {
|
||||
providerId = providerEntry.id;
|
||||
@@ -65,6 +67,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ prov
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const compatibleNode = (await getProviderNodeById(rawProvider)) as {
|
||||
prefix?: unknown;
|
||||
} | null;
|
||||
compatiblePrefix =
|
||||
typeof compatibleNode?.prefix === "string" && compatibleNode.prefix.trim().length > 0
|
||||
? compatibleNode.prefix.trim()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +96,17 @@ export async function GET(request: Request, { params }: { params: Promise<{ prov
|
||||
if (!id) return id;
|
||||
if (id.startsWith(`${providerAlias}/`)) return id.slice(providerAlias.length + 1);
|
||||
if (id.startsWith(`${providerId}/`)) return id.slice(providerId.length + 1);
|
||||
if (compatiblePrefix && id.startsWith(`${compatiblePrefix}/`)) {
|
||||
return id.slice(compatiblePrefix.length + 1);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const filtered = payload.data.filter((model) => model?.owned_by === providerId);
|
||||
const acceptedOwners = new Set([providerId, providerAlias]);
|
||||
if (compatiblePrefix) acceptedOwners.add(compatiblePrefix);
|
||||
const filtered = payload.data.filter(
|
||||
(model) => typeof model?.owned_by === "string" && acceptedOwners.has(model.owned_by)
|
||||
);
|
||||
const deduped = new Map<string, Record<string, any>>();
|
||||
|
||||
for (const model of filtered) {
|
||||
|
||||
@@ -15,7 +15,11 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-v1-provid
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const providerNodesDb = await import("../../src/lib/db/providers/nodes.ts");
|
||||
const serviceModelsDb = await import("../../src/lib/db/serviceModels.ts");
|
||||
const catalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const routeModule = await import("../../src/app/api/v1/providers/[provider]/models/route.ts");
|
||||
|
||||
function makeRequest(provider: string) {
|
||||
@@ -30,6 +34,9 @@ async function callGET(provider: string) {
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
catalog.__resetCatalogBuilderRunsForTest();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
@@ -105,13 +112,57 @@ test("GET /v1/providers/:provider/models returns synced embedded service models"
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(body.object, "list");
|
||||
assert.deepEqual(
|
||||
body.data.map((model: any) => model.id),
|
||||
body.data.map((model: { id: string }) => model.id),
|
||||
["cli/gpt-5"]
|
||||
);
|
||||
assert.equal(body.data[0].owned_by, "cliproxyapi");
|
||||
assert.equal(body.data[0].parent, null);
|
||||
});
|
||||
|
||||
test("#13829: compatible provider IDs return synced models owned by their public prefix", async () => {
|
||||
const providerId = "openai-compatible-chat-a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
||||
const prefix = "hz";
|
||||
const modelId = "Qwen/Qwen3.6-35B-A3B-FP8";
|
||||
|
||||
await providerNodesDb.createProviderNode({
|
||||
id: providerId,
|
||||
type: "openai-compatible-chat",
|
||||
name: "Hetzner",
|
||||
prefix,
|
||||
apiType: "chat",
|
||||
baseUrl: "https://inference.example.com/v1",
|
||||
});
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: providerId,
|
||||
authType: "apikey",
|
||||
name: "Hetzner",
|
||||
apiKey: "test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection(providerId, String(connection.id), [
|
||||
{ id: modelId, name: modelId, source: "imported" },
|
||||
]);
|
||||
|
||||
const catalogResponse = await catalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const catalogBody = (await catalogResponse.json()) as { data: Array<Record<string, unknown>> };
|
||||
const catalogModel = catalogBody.data.find((model) => model.id === `${prefix}/${modelId}`);
|
||||
assert.equal(catalogModel?.owned_by, prefix);
|
||||
|
||||
const res = await callGET(providerId);
|
||||
const body = await res.json();
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(
|
||||
body.data.map((model: { id: string }) => model.id),
|
||||
[modelId]
|
||||
);
|
||||
assert.equal(body.data[0].owned_by, prefix);
|
||||
assert.equal(body.data[0].parent, null);
|
||||
});
|
||||
|
||||
test("GET /v1/providers/:provider/models rejects non-matching connection-like strings", async () => {
|
||||
// Looks like a connection ID but with wrong prefix
|
||||
const res = await callGET("custom-compatible-chat-a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
||||
|
||||
Reference in New Issue
Block a user