mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 03:42:21 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
1033 lines
35 KiB
TypeScript
1033 lines
35 KiB
TypeScript
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-model-sync-route-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
// FASE-01: API_KEY_SECRET is required for CRC operations (no hardcoded fallback)
|
|
if (!process.env.API_KEY_SECRET) {
|
|
process.env.API_KEY_SECRET = "test-model-sync-secret-" + Date.now();
|
|
}
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const localDb = await import("../../src/lib/localDb.ts");
|
|
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const modelsDb = await import("../../src/lib/db/models.ts");
|
|
const callLogs = await import("../../src/lib/usage/callLogs.ts");
|
|
const modelSyncRoute = await import("../../src/app/api/providers/[id]/sync-models/route.ts");
|
|
const scheduler = await import("../../src/shared/services/modelSyncScheduler.ts");
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
async function resetStorage() {
|
|
delete process.env.INITIAL_PASSWORD;
|
|
globalThis.fetch = originalFetch;
|
|
// Reset the shared loopback readiness gate between tests so the cached
|
|
// promise from a previous test doesn't poison this one (PR #2221 adds
|
|
// an __loopbackReadyPromise module-level cache that, once resolved, is
|
|
// reused for the rest of the process). Without this reset, the very
|
|
// first test's mock-fetch resolution (or rejection) leaks into every
|
|
// subsequent test, causing the route to use in-process fallback instead
|
|
// of the test's mocked self-fetch.
|
|
modelSyncRoute.__resetLoopbackReadinessForTests();
|
|
core.resetDbInstance();
|
|
apiKeysDb.resetApiKeyState();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
test.after(() => {
|
|
globalThis.fetch = originalFetch;
|
|
core.resetDbInstance();
|
|
apiKeysDb.resetApiKeyState();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
async function enableAuth() {
|
|
process.env.INITIAL_PASSWORD = "bootstrap-password";
|
|
await localDb.updateSettings({ requireLogin: true, password: "" });
|
|
}
|
|
|
|
test("model sync route skips success log when fetched models do not change stored models", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "MAIN",
|
|
displayName: "OpenRouter Main",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
|
{
|
|
id: "custom-model-1",
|
|
name: "Custom Model 1",
|
|
source: "imported",
|
|
},
|
|
]);
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [{ id: "custom-model-1", name: "Custom Model 1" }],
|
|
});
|
|
};
|
|
|
|
try {
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const body = (await response.json()) as any;
|
|
assert.equal(body.logged, false);
|
|
assert.deepEqual(body.modelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
|
assert.deepEqual(body.models, []);
|
|
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
assert.equal(logs.length, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("model sync route stores the real provider while keeping the account label", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "MAIN",
|
|
displayName: "OpenRouter Main",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [{ id: "custom-model-2", name: "Custom Model 2" }],
|
|
});
|
|
};
|
|
|
|
try {
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const body = (await response.json()) as any;
|
|
assert.equal(body.logged, true);
|
|
assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 });
|
|
assert.equal(body.provider, "openrouter");
|
|
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].provider, "openrouter");
|
|
assert.equal(logs[0].account, "MAIN");
|
|
assert.equal(logs[0].model, "model-sync");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("model sync route requires authentication for external requests when auth is enabled", async () => {
|
|
await resetStorage();
|
|
await enableAuth();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "Protected Connection",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 401);
|
|
assert.equal(body.error.message, "Authentication required");
|
|
assert.equal(body.error.type, "invalid_api_key");
|
|
});
|
|
|
|
test("model sync route returns 404 for unknown connections after internal auth passes", async () => {
|
|
await resetStorage();
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request("http://localhost/api/providers/missing/sync-models", {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: "missing" } }
|
|
);
|
|
|
|
assert.equal(response.status, 404);
|
|
assert.deepEqual(await response.json(), { error: "Connection not found" });
|
|
});
|
|
|
|
test("model sync route propagates upstream failures and records an error log entry", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "Error Branch",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({ error: "Provider upstream unavailable" }, { status: 502 });
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 502);
|
|
assert.equal(body.error, "Provider upstream unavailable");
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 502);
|
|
assert.equal(logs[0].provider, "openrouter");
|
|
assert.equal(logs[0].path, `/api/providers/${connection.id}/models`);
|
|
});
|
|
|
|
test("model sync route falls back to the upstream HTTP status when the models payload has no error field", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "Rate Limited Sync",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({}, { status: 429 });
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 429);
|
|
assert.equal(body.error, "Failed to fetch models");
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 429);
|
|
assert.equal(logs[0].error, "HTTP 429");
|
|
});
|
|
|
|
test("model sync route reports invalid JSON /models responses without losing upstream status", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "Invalid JSON Sync",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return new Response("<html>bad gateway</html>", {
|
|
status: 200,
|
|
headers: { "content-type": "text/html" },
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 502);
|
|
assert.equal(body.error, "Invalid JSON response from /models");
|
|
assert.equal(body.upstreamStatus, 200);
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 200);
|
|
assert.equal(logs[0].error, "Invalid JSON response from /models");
|
|
});
|
|
|
|
test("model sync route preserves previously synced models when the upstream omits the models list", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "No Models Returned",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
|
{
|
|
id: "persisted-model",
|
|
name: "Persisted Model",
|
|
source: "imported",
|
|
},
|
|
]);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.syncedModels, 1);
|
|
assert.equal(body.logged, false);
|
|
assert.deepEqual(body.modelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
|
assert.deepEqual(body.models, []);
|
|
assert.deepEqual(await modelsDb.getSyncedAvailableModels("openrouter"), [
|
|
{
|
|
id: "persisted-model",
|
|
name: "Persisted Model",
|
|
source: "imported",
|
|
},
|
|
]);
|
|
assert.equal(logs.length, 0);
|
|
});
|
|
|
|
test("model sync route writes synced available models for Gemini connections", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "gemini",
|
|
authType: "apikey",
|
|
name: "Gemini Sync",
|
|
apiKey: "gm-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
id: "gemini-custom-preview",
|
|
name: "Gemini Custom Preview",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
inputTokenLimit: 32768,
|
|
outputTokenLimit: 8192,
|
|
description: "Custom Gemini preview model",
|
|
supportsThinking: true,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const synced = await modelsDb.getSyncedAvailableModels("gemini");
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "gemini");
|
|
assert.equal(body.syncedModels, 1);
|
|
assert.equal(body.logged, true);
|
|
assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 });
|
|
assert.deepEqual(body.models, []);
|
|
assert.deepEqual(synced, [
|
|
{
|
|
id: "gemini-custom-preview",
|
|
name: "Gemini Custom Preview",
|
|
source: "imported",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
inputTokenLimit: 32768,
|
|
outputTokenLimit: 8192,
|
|
description: "Custom Gemini preview model",
|
|
supportsThinking: true,
|
|
},
|
|
]);
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 200);
|
|
});
|
|
|
|
test("model sync route writes synced available models for non-Gemini providers too", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "opencode-go",
|
|
authType: "apikey",
|
|
name: "OpenCode Go Sync",
|
|
apiKey: "opencode-go-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
id: "glm-5.1",
|
|
name: "GLM 5.1",
|
|
supportedEndpoints: ["chat"],
|
|
inputTokenLimit: 262144,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const synced = await modelsDb.getSyncedAvailableModels("opencode-go");
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "opencode-go");
|
|
assert.equal(body.syncedModels, 1);
|
|
assert.deepEqual(synced, [
|
|
{
|
|
id: "glm-5.1",
|
|
name: "GLM 5.1",
|
|
source: "imported",
|
|
supportedEndpoints: ["chat"],
|
|
inputTokenLimit: 262144,
|
|
},
|
|
]);
|
|
});
|
|
|
|
// #10603 ("make upstream model sync opt-in and preserve manual overrides") also applies to
|
|
// import/merge mode: a manual custom-model row sharing an id with a discovered model is kept
|
|
// as the user-owned overlay instead of being demoted/removed. See the comment above the
|
|
// "reports synced managed models separately from preserved manual models" test.
|
|
test("model sync route import mode merges discovered models without deleting manual models", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "OpenRouter Import",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.addCustomModel("openrouter", "manual-only", "Manual Only", "manual");
|
|
await modelsDb.addCustomModel("openrouter", "router-v4", "Manual Router V4", "manual");
|
|
await localDb.setModelAlias("manual-only", "openrouter/manual-only");
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [{ id: "router-v4", name: "Router V4" }],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models?mode=import`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const aliases = await localDb.getModelAliases();
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.mode, "merge");
|
|
assert.equal(body.importedCount, 1);
|
|
assert.equal(body.updatedCount, 0);
|
|
assert.equal(body.syncedAliases, 1);
|
|
assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 });
|
|
assert.deepEqual(body.customModelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
|
assert.deepEqual(
|
|
body.models.map((model) => ({ id: model.id, source: model.source })),
|
|
[
|
|
{ id: "manual-only", source: "manual" },
|
|
{ id: "router-v4", source: "manual" },
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
body.importedModels.map((model) => ({ id: model.id, source: model.source })),
|
|
[{ id: "router-v4", source: "imported" }]
|
|
);
|
|
assert.deepEqual(
|
|
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
|
id: model.id,
|
|
source: model.source,
|
|
})),
|
|
[{ id: "router-v4", source: "imported" }]
|
|
);
|
|
assert.equal(aliases["manual-only"], "openrouter/manual-only");
|
|
assert.equal(aliases["router-v4"], "openrouter/router-v4");
|
|
});
|
|
|
|
test("model sync route import mode ignores supported endpoint ordering changes", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "OpenRouter Import Stable",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
|
{
|
|
id: "router-v4",
|
|
name: "Router V4",
|
|
source: "imported",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
},
|
|
]);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
id: "router-v4",
|
|
name: "Router V4",
|
|
supportedEndpoints: ["embeddings", "chat"],
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models?mode=import`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.importedCount, 0);
|
|
assert.equal(body.updatedCount, 0);
|
|
assert.deepEqual(body.importedChanges, { added: 0, updated: 0, unchanged: 1, total: 0 });
|
|
assert.deepEqual(body.modelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
|
assert.equal(body.logged, false);
|
|
assert.deepEqual(body.importedModels, []);
|
|
assert.deepEqual(
|
|
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
|
id: model.id,
|
|
supportedEndpoints: model.supportedEndpoints,
|
|
})),
|
|
[{ id: "router-v4", supportedEndpoints: ["chat", "embeddings"] }]
|
|
);
|
|
assert.deepEqual(body.models, []);
|
|
assert.equal(logs.length, 0);
|
|
});
|
|
|
|
test("model sync route import mode reports updates without counting them as new imports", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "OpenRouter Import Update",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
|
{
|
|
id: "router-v4",
|
|
name: "Router V4",
|
|
source: "imported",
|
|
supportedEndpoints: ["chat"],
|
|
},
|
|
]);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
id: "router-v4",
|
|
name: "Router V4 Updated",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models?mode=import`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.importedCount, 0);
|
|
assert.equal(body.updatedCount, 1);
|
|
assert.deepEqual(body.importedChanges, { added: 0, updated: 1, unchanged: 0, total: 1 });
|
|
assert.deepEqual(body.importedModels, []);
|
|
assert.deepEqual(
|
|
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
|
id: model.id,
|
|
name: model.name,
|
|
supportedEndpoints: model.supportedEndpoints,
|
|
})),
|
|
[
|
|
{
|
|
id: "router-v4",
|
|
name: "Router V4 Updated",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
},
|
|
]
|
|
);
|
|
assert.deepEqual(body.models, []);
|
|
assert.equal(body.logged, true);
|
|
assert.equal(logs.length, 1);
|
|
});
|
|
|
|
test("model sync route records added, removed, and updated model diffs with fallback identifiers", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "oauth",
|
|
email: "sync@example.com",
|
|
accessToken: "sync-token",
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", connection.id, [
|
|
{
|
|
id: "persisted-model",
|
|
name: "Persisted Model",
|
|
source: "imported",
|
|
},
|
|
{
|
|
id: "removed-model",
|
|
name: "Removed Model",
|
|
source: "imported",
|
|
},
|
|
]);
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
id: "persisted-model",
|
|
name: "Persisted Model v2",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
},
|
|
{
|
|
model: "fallback-model",
|
|
displayName: "Fallback Model",
|
|
description: "Fallback from model field",
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.syncedModels, 2);
|
|
assert.equal(body.logged, true);
|
|
assert.deepEqual(body.modelChanges, { added: 1, removed: 1, updated: 1, total: 3 });
|
|
assert.deepEqual(
|
|
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
|
id: model.id,
|
|
name: model.name,
|
|
supportedEndpoints: model.supportedEndpoints,
|
|
description: model.description,
|
|
})),
|
|
[
|
|
{
|
|
id: "persisted-model",
|
|
name: "Persisted Model v2",
|
|
supportedEndpoints: ["chat", "embeddings"],
|
|
description: undefined,
|
|
},
|
|
{
|
|
id: "fallback-model",
|
|
name: "Fallback Model",
|
|
supportedEndpoints: undefined,
|
|
description: "Fallback from model field",
|
|
},
|
|
]
|
|
);
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 200);
|
|
assert.equal(logs[0].provider, "openrouter");
|
|
assert.equal(logs[0].account, "sync@example.com");
|
|
});
|
|
|
|
test("model sync route forwards cookies, filters built-ins, and syncs aliases for internal requests", async () => {
|
|
await resetStorage();
|
|
await enableAuth();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "External Sync",
|
|
displayName: "OpenRouter External",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await localDb.setModelAlias("stale-model", "openrouter/stale-model");
|
|
await localDb.setModelAlias("router-v2", "other-provider/router-v2");
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
assert.equal(init.headers.cookie, "session=test-cookie");
|
|
assert.equal(
|
|
init.headers[scheduler.getModelSyncInternalAuthHeaderName()],
|
|
scheduler.buildModelSyncInternalHeaders()[scheduler.getModelSyncInternalAuthHeaderName()]
|
|
);
|
|
|
|
return Response.json({
|
|
models: [
|
|
{ id: "auto", name: "Auto (Best Available)" },
|
|
{ id: "router-v2", name: "Router V2" },
|
|
{ id: "router-v3", name: "Router V3" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: {
|
|
cookie: "session=test-cookie",
|
|
...scheduler.buildModelSyncInternalHeaders(),
|
|
},
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const aliases = await localDb.getModelAliases();
|
|
const logs = await callLogs.getCallLogs({ model: "model-sync", limit: 10 });
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "openrouter");
|
|
assert.equal(body.syncedModels, 3);
|
|
assert.equal(body.availableModelsCount, 3);
|
|
assert.equal(body.syncedAliases, 3);
|
|
assert.equal(body.logged, true);
|
|
assert.deepEqual(body.modelChanges, { added: 3, removed: 0, updated: 0, total: 3 });
|
|
assert.deepEqual(body.models, []);
|
|
assert.equal(aliases["stale-model"], undefined);
|
|
assert.equal(aliases["auto"], "openrouter/auto");
|
|
assert.equal(aliases["openrouter-router-v2"], "openrouter/router-v2");
|
|
assert.equal(aliases["router-v3"], "openrouter/router-v3");
|
|
assert.equal(logs.length, 1);
|
|
assert.equal(logs[0].status, 200);
|
|
assert.equal(logs[0].account, "External Sync");
|
|
});
|
|
|
|
// #10603 ("make upstream model sync opt-in and preserve manual overrides") changed
|
|
// importManagedModels() so a manually configured custom-model row that shares an id with
|
|
// a synced upstream model is no longer demoted/removed — it stays as the user-owned
|
|
// metadata overlay and is merged over the synced base at read time (see the comment in
|
|
// src/lib/providerModels/managedModelImport.ts). Only rows already tagged as an
|
|
// imported/auto-sync source get pruned. This test predates that change; its expectations
|
|
// below reflect the current preserve-manual-overrides behavior.
|
|
test("model sync route reports synced managed models separately from preserved manual models", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openrouter",
|
|
authType: "apikey",
|
|
name: "Mixed Sync",
|
|
apiKey: "test-key",
|
|
});
|
|
|
|
await modelsDb.addCustomModel("openrouter", "manual-only", "Manual Only", "manual");
|
|
await modelsDb.addCustomModel("openrouter", "router-v4", "Manual Router V4", "manual");
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [{ id: "router-v4", name: "Router V4" }],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.syncedModels, 1);
|
|
assert.equal(body.availableModelsCount, 2);
|
|
assert.equal(body.importedCount, 1);
|
|
assert.equal(body.updatedCount, 0);
|
|
assert.deepEqual(body.customModelChanges, { added: 0, removed: 0, updated: 0, total: 0 });
|
|
assert.deepEqual(
|
|
body.models.map((model) => ({ id: model.id, source: model.source })),
|
|
[
|
|
{ id: "manual-only", source: "manual" },
|
|
{ id: "router-v4", source: "manual" },
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
(await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({
|
|
id: model.id,
|
|
source: model.source,
|
|
})),
|
|
[{ id: "router-v4", source: "imported" }]
|
|
);
|
|
});
|
|
|
|
test("model sync route uses provider-node prefixes when syncing compatible-provider aliases", async () => {
|
|
await resetStorage();
|
|
|
|
await providersDb.createProviderNode({
|
|
id: "anthropic-compatible-demo",
|
|
type: "anthropic-compatible",
|
|
name: "Anthropic Demo",
|
|
prefix: "cm",
|
|
baseUrl: "https://proxy.example.com",
|
|
chatPath: "/v1/messages",
|
|
modelsPath: "/v1/models",
|
|
});
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "anthropic-compatible-demo",
|
|
authType: "apikey",
|
|
name: "Compatible Sync",
|
|
apiKey: "compat-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com",
|
|
chatPath: "/v1/messages",
|
|
modelsPath: "/v1/models",
|
|
},
|
|
});
|
|
|
|
await localDb.setModelAlias("sonnet-4-6", "some-other-provider/sonnet-4-6");
|
|
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("__readiness_probe__")) return new Response(null, { status: 404 });
|
|
assert.equal(
|
|
String(url),
|
|
`http://127.0.0.1:20128/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`
|
|
);
|
|
return Response.json({
|
|
models: [{ id: "sonnet-4-6", name: "Sonnet 4.6" }],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as any;
|
|
const aliases = await localDb.getModelAliases();
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "anthropic-compatible-demo");
|
|
assert.equal(body.syncedAliases, 1);
|
|
assert.equal(aliases["cm-sonnet-4-6"], "anthropic-compatible-demo/sonnet-4-6");
|
|
});
|
|
|
|
test("model sync route falls back to in-process discovery when internal self-fetch throws", async () => {
|
|
await resetStorage();
|
|
|
|
const connection = await providersDb.createProviderConnection({
|
|
provider: "openai-compatible-aio",
|
|
authType: "apikey",
|
|
name: "AIO Import",
|
|
apiKey: "test-key",
|
|
providerSpecificData: {
|
|
prefix: "aio",
|
|
apiType: "chat",
|
|
baseUrl: "https://api.bltcy.ai/v1",
|
|
nodeName: "aio",
|
|
autoSync: true,
|
|
},
|
|
});
|
|
|
|
// Reset shared readiness gate so this test exercises the probe path cleanly.
|
|
modelSyncRoute.__resetLoopbackReadinessForTests();
|
|
|
|
const fetchCalls: string[] = [];
|
|
globalThis.fetch = async (url) => {
|
|
const urlString = String(url);
|
|
|
|
// Loopback readiness probe: respond 404 so the gate opens immediately.
|
|
// (Any HTTP response confirms the server is up — see ensureLoopbackServerReady.)
|
|
if (urlString.includes("__readiness_probe__")) {
|
|
return new Response(null, { status: 404 });
|
|
}
|
|
|
|
fetchCalls.push(urlString);
|
|
|
|
if (urlString.includes("/models?refresh=true&excludeCustom=true")) {
|
|
throw new Error("fetch failed");
|
|
}
|
|
|
|
assert.equal(urlString, "https://api.bltcy.ai/v1/models");
|
|
return Response.json({
|
|
data: [{ id: "aio-model", name: "AIO Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await modelSyncRoute.POST(
|
|
new Request(`http://localhost/api/providers/${connection.id}/sync-models?mode=import`, {
|
|
method: "POST",
|
|
headers: scheduler.buildModelSyncInternalHeaders(),
|
|
}),
|
|
{ params: { id: connection.id } }
|
|
);
|
|
const body = (await response.json()) as {
|
|
importedCount: number;
|
|
importedModels: Array<{ id: string; source: string }>;
|
|
};
|
|
const customModels = (await modelsDb.getCustomModels("openai-compatible-aio")) as Array<{
|
|
id: string;
|
|
source: string;
|
|
}>;
|
|
const availableModels = await modelsDb.getSyncedAvailableModels("openai-compatible-aio");
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.importedCount, 1);
|
|
assert.deepEqual(
|
|
body.importedModels.map((model) => ({ id: model.id, source: model.source })),
|
|
[{ id: "aio-model", source: "imported" }]
|
|
);
|
|
assert.deepEqual(
|
|
customModels.map((model) => ({ id: model.id, source: model.source })),
|
|
[]
|
|
);
|
|
assert.deepEqual(
|
|
availableModels.map((model) => ({ id: model.id, source: model.source })),
|
|
[{ id: "aio-model", source: "imported" }]
|
|
);
|
|
// selfFetchWithRetry default maxRetries=3: all 3 attempts throw, then in-process
|
|
// fallback fires (which triggers the upstream bltcy.ai fetch). So fetchCalls
|
|
// contains 3 self-fetch URLs followed by 1 upstream URL.
|
|
// Route forces IPv4 origin (http://127.0.0.1:PORT) — never "localhost" — to avoid
|
|
// ::1 (IPv6) resolution issues in containers. PORT defaults to 20128 when env unset.
|
|
const expectedPort = process.env.OMNIROUTE_PORT || process.env.PORT || "20128";
|
|
const selfFetchUrl = `http://127.0.0.1:${expectedPort}/api/providers/${connection.id}/models?refresh=true&excludeCustom=true`;
|
|
assert.equal(
|
|
fetchCalls.slice(0, 3).every((u) => u === selfFetchUrl),
|
|
true,
|
|
"first 3 calls should be self-fetch retries"
|
|
);
|
|
assert.equal(fetchCalls[3], "https://api.bltcy.ai/v1/models", "4th call should be upstream");
|
|
assert.equal(fetchCalls.length, 4, "should have exactly 3 retries + 1 upstream call");
|
|
});
|