Files
OmniRoute/tests/unit/models-catalog-route.test.ts
Diego Rodrigues de Sa e Souza 6da2418247 chore(providers): remove a keyless provider integration at its operator's request (#12440)
The service operator asked in writing (2026-08-30) that their service be
removed from OmniRoute entirely: executor, registry entry, no-auth catalog
entry and alias, icon mapping, env var, docs rows, dedicated tests and
snapshots, and every passing mention in comments, fixtures and CHANGELOG
entries. Provider count drops from 355 to 354 on every canonical surface.

Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
2026-09-02 07:11:38 -03:00

1652 lines
57 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-catalog-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-test-secret";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const featureFlagsDb = await import("../../src/lib/db/featureFlags.ts");
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
// #6408 added a 1.5s TTL response cache to getUnifiedModelsResponse keyed only by
// (prefix, isCodex client, apiKey) — NOT by DB/settings state. Without clearing it
// between test cases, a test running within the TTL window of a previous one gets
// served the previous test's stale serialized catalog instead of a fresh build.
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
}
async function seedConnection(provider: string, overrides: Record<string, unknown> = {}) {
return providersDb.createProviderConnection({
provider,
authType: (overrides.authType as string) || "apikey",
name: (overrides.name as string) || `${provider}-${Math.random().toString(16).slice(2, 8)}`,
apiKey: (overrides.apiKey as string) || "sk-test",
accessToken: overrides.accessToken as string | undefined,
isActive: (overrides.isActive as boolean) ?? true,
testStatus: (overrides.testStatus as string) || "active",
providerSpecificData: (overrides.providerSpecificData as Record<string, unknown>) || {},
});
}
function capability(overrides = {}) {
return {
tool_call: null,
reasoning: null,
attachment: null,
structured_output: null,
temperature: null,
modalities_input: JSON.stringify([]),
modalities_output: JSON.stringify([]),
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: null,
limit_input: null,
limit_output: null,
interleaved_field: null,
...overrides,
};
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("v1 models catalog requires auth when the route is protected and login is enabled", async () => {
await settingsDb.updateSettings({
requireLogin: true,
password: "hashed-password",
requireAuthForModels: true,
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
assert.equal(response.status, 401);
assert.equal(body.error.code, "invalid_api_key");
assert.match(body.error.message, /Authentication required/i);
});
test("v1 models catalog accepts bearer API keys and filters the list by allowed model patterns", async () => {
await settingsDb.updateSettings({
requireLogin: true,
password: "hashed-password",
requireAuthForModels: true,
});
await seedConnection("openai", { name: "openai-main" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-main",
apiKey: null,
accessToken: "claude-access",
});
const key = await apiKeysDb.createApiKey("catalog-filter", "machine-catalog");
await apiKeysDb.updateApiKeyPermissions(key.id, {
allowedModels: ["openai/*"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models", {
headers: { Authorization: `Bearer ${key.key}` },
})
);
const body = (await response.json()) as any;
const ids = body.data.map((item) => item.id);
assert.equal(response.status, 200);
assert.ok(ids.some((id) => id.startsWith("openai/")));
assert.equal(
ids.some((id) => id.startsWith("claude/") || id.startsWith("cc/")),
false
);
});
test("v1 models catalog does NOT accept API keys supplied via query string (#3300 security follow-up)", async () => {
// Query-string token fallbacks (`?token=`/`?key=`/`?apiKey=`/`?api_key=`) were
// intentionally removed — a credential in the query string leaks into access
// logs / Referer headers. The VS Code integration uses the path-scoped
// `/vscode/<token>/…` form instead (covered by the next test). So a `?token=`
// on the catalog route is no longer a usable credential → auth fails.
await settingsDb.updateSettings({
requireLogin: true,
password: "hashed-password",
requireAuthForModels: true,
});
await seedConnection("openai", { name: "openai-query-auth" });
const key = await apiKeysDb.createApiKey("catalog-query-auth", "machine-catalog-query");
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request(`http://localhost/api/v1/models?token=${encodeURIComponent(key.key)}`)
);
assert.equal(response.status, 401);
});
test("v1 models catalog accepts API keys embedded in vscode path aliases when auth is required", async () => {
await settingsDb.updateSettings({
requireLogin: true,
password: "hashed-password",
requireAuthForModels: true,
});
await seedConnection("openai", { name: "openai-path-auth" });
const key = await apiKeysDb.createApiKey("catalog-path-auth", "machine-catalog-path");
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/models`)
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.data));
assert.ok(body.data.length > 0);
});
test("v1 models catalog includes display names by default", async () => {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const model = body.data.find((item) => item.id === "oc/big-pickle");
assert.equal(response.status, 200);
assert.ok(model);
assert.equal(model.name, "Big Pickle");
});
test("v1 models catalog omits display names when the feature flag is disabled", async () => {
featureFlagsDb.setFeatureFlagOverride("MODEL_CATALOG_INCLUDE_NAMES", "false");
try {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const model = body.data.find((item) => item.id === "oc/big-pickle");
assert.equal(response.status, 200);
assert.ok(model);
assert.equal("name" in model, false);
assert.equal(model.root, "big-pickle");
} finally {
featureFlagsDb.removeFeatureFlagOverride("MODEL_CATALOG_INCLUDE_NAMES");
}
});
test("v1 models catalog hides models excluded by every active connection while keeping models served by at least one account", async () => {
const first = await seedConnection("openai", {
name: "openai-first",
providerSpecificData: {
excludedModels: ["gpt-5.4*"],
},
});
const second = await seedConnection("openai", {
name: "openai-second",
providerSpecificData: {
excludedModels: ["gpt-4.1*"],
},
});
let response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
let body = (await response.json()) as any;
let ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.equal(ids.has("openai/gpt-5.4-mini"), true);
await providersDb.updateProviderConnection((second as any).id, {
providerSpecificData: {
excludedModels: ["gpt-5.4*"],
},
});
response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
body = (await response.json()) as any;
ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.equal(ids.has("openai/gpt-5.4-mini"), false);
await providersDb.updateProviderConnection((first as any).id, {
providerSpecificData: {
excludedModels: [],
},
});
});
test("v1 models catalog includes combos and custom models while excluding hidden models and blocked providers", async () => {
await settingsDb.updateSettings({
blockedProviders: ["claude"],
});
await seedConnection("openai", { name: "openai-visible" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-blocked",
apiKey: null,
accessToken: "claude-access",
});
await seedConnection("kiro", {
authType: "oauth",
name: "kiro-custom",
apiKey: null,
accessToken: "kiro-access",
});
modelsDb.mergeModelCompatOverride("openai", "gpt-4o-mini", { isHidden: true });
await modelsDb.addCustomModel("kiro", "custom-kiro", "Custom Kiro");
await combosDb.createCombo({
name: "team-router",
strategy: "priority",
models: ["openai/gpt-4o"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.ok(ids.has("team-router"));
assert.ok(ids.has("kr/custom-kiro"));
assert.ok(ids.has("kiro/custom-kiro"));
assert.equal(ids.has("openai/gpt-4o-mini"), false);
assert.equal(
[...ids].some((id) => (id as any).startsWith("claude/") || (id as any).startsWith("cc/")),
false
);
});
test("v1 models catalog keeps only visible combos when no providers are active", async () => {
const visible = await combosDb.createCombo({
name: "visible-combo",
strategy: "priority",
models: ["openai/gpt-4o"],
});
await combosDb.updateCombo((visible as any).id, { context_length: 32000 });
const hidden = await combosDb.createCombo({
name: "hidden-combo",
strategy: "priority",
models: ["openai/gpt-4o"],
isHidden: true,
});
const inactive = await combosDb.createCombo({
name: "inactive-combo",
strategy: "priority",
models: ["openai/gpt-4o"],
});
await combosDb.updateCombo((inactive as any).id, { isActive: false });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
// The visible combo must be present (noAuth provider models may also appear — that is correct
// behavior after the fix for Issue #2798, so we check membership rather than exact equality).
const ids = body.data.map((item) => item.id);
assert.ok(ids.includes(visible.name), "visible combo must appear");
const visibleCombo = body.data.find((item) => item.id === visible.name);
assert.ok(visibleCombo, "visible combo entry must exist");
assert.equal(visibleCombo.context_length, 32000);
assert.equal(
body.data.some((item) => item.id === hidden.name),
false
);
assert.equal(
body.data.some((item) => item.id === inactive.name),
false
);
});
test("v1 models catalog derives combo metadata from known targets conservatively", async () => {
try {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"combo-alpha": capability({
tool_call: true,
reasoning: true,
attachment: true,
structured_output: true,
temperature: false,
modalities_input: JSON.stringify(["text", "image"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 1000,
limit_input: 900,
limit_output: 120,
}),
},
gemini: {
"combo-beta": capability({
tool_call: true,
reasoning: true,
attachment: false,
structured_output: true,
temperature: false,
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 800,
limit_input: 700,
limit_output: 90,
}),
},
});
await combosDb.createCombo({
name: "metadata-router",
strategy: "priority",
models: ["openai/combo-alpha", "gemini/combo-beta"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const combo = body.data.find((item) => item.id === "metadata-router");
assert.equal(response.status, 200);
assert.ok(combo);
assert.equal(combo.context_length, 800);
assert.equal(combo.max_input_tokens, 700);
assert.equal(combo.max_output_tokens, 90);
assert.deepEqual(combo.input_modalities, ["text"]);
assert.deepEqual(combo.output_modalities, ["text"]);
assert.equal(combo.capabilities.structured_output, true);
assert.equal(combo.capabilities.temperature, false);
assert.equal(combo.capabilities.tool_calling, true);
assert.equal(combo.capabilities.reasoning, true);
assert.equal(combo.capabilities.thinking, true);
assert.equal("vision" in combo.capabilities, false);
assert.equal("attachment" in combo.capabilities, false);
assert.equal("architecture" in combo, false);
assert.equal("top_provider" in combo, false);
assert.equal("supported_parameters" in combo, false);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog lets explicit combo context override derived context", async () => {
try {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"context-alpha": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 1000,
limit_input: 900,
limit_output: 120,
}),
},
gemini: {
"context-beta": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 800,
limit_input: 700,
limit_output: 90,
}),
},
});
const combo = await combosDb.createCombo({
name: "context-router",
strategy: "priority",
models: ["openai/context-alpha", "gemini/context-beta"],
});
await combosDb.updateCombo((combo as any).id, { context_length: 12345 });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const listed = body.data.find((item) => item.id === "context-router");
assert.equal(response.status, 200);
assert.equal(listed.context_length, 12345);
assert.equal(listed.max_input_tokens, 700);
assert.equal(listed.max_output_tokens, 90);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog keeps unknown combo targets visible without guessed metadata", async () => {
await combosDb.createCombo({
name: "unknown-router",
strategy: "priority",
models: ["openai/no-known-metadata"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const combo = body.data.find((item) => item.id === "unknown-router");
assert.equal(response.status, 200);
assert.ok(combo);
assert.equal("context_length" in combo, false);
assert.equal("max_input_tokens" in combo, false);
assert.equal("max_output_tokens" in combo, false);
assert.equal("input_modalities" in combo, false);
assert.equal("output_modalities" in combo, false);
assert.equal("capabilities" in combo, false);
});
test("v1 models catalog aggregates nested combos and keeps hidden child combos unlisted", async () => {
try {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"nested-alpha": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 1000,
limit_input: 900,
limit_output: 120,
}),
},
gemini: {
"nested-beta": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 800,
limit_input: 700,
limit_output: 90,
}),
},
});
await combosDb.createCombo({
name: "hidden-child-router",
strategy: "priority",
models: ["openai/nested-alpha", "gemini/nested-beta"],
isHidden: true,
});
await combosDb.createCombo({
name: "parent-router",
strategy: "priority",
models: ["hidden-child-router"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const parent = body.data.find((item) => item.id === "parent-router");
assert.equal(response.status, 200);
assert.ok(parent);
assert.equal(parent.context_length, 800);
assert.equal(parent.max_output_tokens, 90);
assert.equal(
body.data.some((item) => item.id === "hidden-child-router"),
false
);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog resolves provider aliases without corrupting slashful model ids", async () => {
try {
modelsDevSync.saveModelsDevCapabilities({
claude: {
"alias-model": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 2000,
limit_input: 1900,
limit_output: 200,
}),
},
openrouter: {
"Qwen/Qwen3-Coder": capability({
modalities_input: JSON.stringify(["text"]),
modalities_output: JSON.stringify(["text"]),
limit_context: 1600,
limit_input: 1500,
limit_output: 150,
}),
},
});
await combosDb.createCombo({
name: "alias-and-slash-router",
strategy: "priority",
models: [
{ kind: "model", providerId: "claude", model: "cc/alias-model" },
{ kind: "model", providerId: "openrouter", model: "Qwen/Qwen3-Coder" },
],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const combo = body.data.find((item) => item.id === "alias-and-slash-router");
assert.equal(response.status, 200);
assert.ok(combo);
assert.equal(combo.context_length, 1600);
assert.equal(combo.max_input_tokens, 1500);
assert.equal(combo.max_output_tokens, 150);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog does not final-enrich combo names as real models", async () => {
await combosDb.createCombo({
name: "gpt-5.5",
strategy: "priority",
models: ["openai/no-known-metadata"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const combo = body.data.find((item) => item.id === "gpt-5.5");
assert.equal(response.status, 200);
assert.ok(combo);
assert.equal(combo.owned_by, "combo");
assert.equal("max_output_tokens" in combo, false);
assert.equal("capabilities" in combo, false);
});
test("v1 models catalog exposes claude alias and provider-prefixed built-in models with vision metadata", async () => {
await seedConnection("claude", {
authType: "oauth",
name: "claude-vision",
apiKey: null,
accessToken: "claude-access",
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const aliasModel = body.data.find((item) => item.id === "cc/claude-sonnet-4-6");
const providerModel = body.data.find((item) => item.id === "claude/claude-sonnet-4-6");
assert.equal(response.status, 200);
assert.ok(aliasModel);
assert.ok(providerModel);
assert.equal(providerModel.parent, aliasModel.id);
assert.equal(aliasModel.capabilities?.vision, true);
assert.deepEqual(aliasModel.input_modalities, ["text", "image"]);
assert.deepEqual(aliasModel.output_modalities, ["text"]);
});
test("v1 models catalog exposes refreshed GitHub Copilot aliases and drops retired models", async () => {
await seedConnection("github", {
authType: "oauth",
name: "github-current",
apiKey: null,
accessToken: "github-access",
providerSpecificData: {
copilotToken: "copilot-token",
},
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const aliasModel = body.data.find((item) => item.id === "gh/gpt-5.4");
const providerModel = body.data.find((item) => item.id === "github/gpt-5.4");
const codexModel = body.data.find((item) => item.id === "gh/gpt-5.3-codex");
const opusModel = body.data.find((item) => item.id === "github/claude-opus-4.7");
assert.equal(response.status, 200);
assert.ok(aliasModel);
assert.ok(providerModel);
assert.ok(codexModel);
assert.ok(opusModel);
assert.equal(providerModel.parent, aliasModel.id);
assert.equal(
body.data.some((item) => item.id === "gh/gpt-5.1"),
false
);
assert.equal(
body.data.some((item) => item.id === "gh/claude-opus-4.1"),
false
);
});
test("v1 models catalog exposes bare Codex-preferred IDs for native Codex clients", async () => {
await seedConnection("codex", {
authType: "oauth",
name: "codex-native",
apiKey: null,
accessToken: "codex-access",
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const getModel = (id: string) => body.data.find((item) => item.id === id);
assert.equal(response.status, 200);
const modelId = "codex-auto-review";
const bareModel = getModel(modelId);
const providerModel = getModel(`codex/${modelId}`);
const aliasModel = getModel(`cx/${modelId}`);
const openAiModel = getModel(`openai/${modelId}`);
assert.ok(bareModel, `expected bare ${modelId} model`);
assert.ok(providerModel, `expected codex/${modelId} model`);
assert.ok(aliasModel, `expected cx/${modelId} model`);
assert.equal(openAiModel, undefined);
assert.equal(bareModel.owned_by, "codex");
assert.equal(bareModel.parent, providerModel.id);
assert.equal(providerModel.parent, aliasModel.id);
});
test("v1 models catalog exposes current Antigravity aliases without retired model IDs", async () => {
await seedConnection("antigravity", {
authType: "oauth",
name: "antigravity-preview",
apiKey: null,
accessToken: "antigravity-access",
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.equal(ids.has("antigravity/gemini-3-pro-preview"), false);
assert.equal(ids.has("antigravity/gemini-3.1-pro"), false);
assert.equal(ids.has("antigravity/gemini-2.5-computer-use-preview-10-2025"), false);
assert.equal(ids.has("antigravity/rev19-uic3-1p"), false);
assert.ok(ids.has("antigravity/gemini-3.7-flash-high"));
assert.ok(ids.has("antigravity/gemini-3.7-flash-medium"));
assert.ok(ids.has("antigravity/gemini-3.7-flash-low"));
assert.equal(ids.has("antigravity/gemini-3.6-flash-high"), false);
assert.equal(ids.has("antigravity/gemini-3.6-flash-medium"), false);
assert.equal(ids.has("antigravity/gemini-3.6-flash-low"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-extra-low"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-low"), false);
assert.equal(ids.has("antigravity/gemini-3-flash-agent"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-medium"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-high"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-preview"), false);
assert.equal(ids.has("antigravity/gemini-3-flash-preview"), false);
assert.equal(ids.has("antigravity/gemini-3.1-pro-high"), false);
assert.ok(ids.has("antigravity/gemini-pro-agent"));
assert.equal(ids.has("antigravity/gemini-claude-sonnet-4-5"), false);
assert.equal(ids.has("antigravity/gemini-claude-sonnet-4-5-thinking"), false);
assert.equal(ids.has("antigravity/gemini-claude-opus-4-5-thinking"), false);
});
test("v1 models catalog uses provider-node prefixes for compatible provider custom models", async () => {
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",
});
await seedConnection("anthropic-compatible-demo", {
name: "anthropic-node",
providerSpecificData: {
baseUrl: "https://proxy.example.com",
chatPath: "/v1/messages",
modelsPath: "/v1/models",
},
});
await modelsDb.addCustomModel("anthropic-compatible-demo", "claude-edge", "Claude Edge");
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.ok(ids.has("cm/claude-edge"));
assert.equal(ids.has("anthropic-compatible-demo/claude-edge"), false);
});
test("v1 models catalog includes synced Gemini models and duplicates audio models for speech", async () => {
const connection = await seedConnection("gemini", {
name: "gemini-synced",
apiKey: "gm-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection(
"gemini" as any,
(connection as any).id,
[
{
id: "gemini-audio-live",
name: "Gemini Audio Live",
source: "imported",
supportedEndpoints: ["audio"],
inputTokenLimit: 4096,
},
{
id: "text-embedding-004",
name: "Text Embedding 004",
source: "imported",
supportedEndpoints: ["embeddings"],
inputTokenLimit: 2048,
},
{
id: "gemini-hidden",
name: "Gemini Hidden",
source: "imported",
supportedEndpoints: ["chat"],
},
]
);
modelsDb.mergeModelCompatOverride("gemini", "gemini-hidden", { isHidden: true });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const audioVariants = body.data.filter((item) => item.id === "gemini/gemini-audio-live");
const embedding = body.data.find((item) => item.id === "gemini/text-embedding-004");
assert.equal(response.status, 200);
assert.equal(audioVariants.length, 2);
assert.deepEqual(audioVariants.map((item) => item.subtype).sort(), ["speech", "transcription"]);
assert.equal(embedding.type, "embedding");
assert.equal(
body.data.some((item) => item.id === "gemini/gemini-hidden"),
false
);
});
test("v1 models catalog keeps Gemini chat models untyped when synced endpoints are omitted", async () => {
const connection = await seedConnection("gemini", {
name: "gemini-chat-default",
apiKey: "gm-chat-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("gemini", (connection as any).id, [
{
id: "gemini-2.5-pro-live",
name: "Gemini 2.5 Pro Live",
source: "imported",
inputTokenLimit: 8192,
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const chatModel = body.data.find((item) => item.id === "gemini/gemini-2.5-pro-live");
assert.equal(response.status, 200);
assert.ok(chatModel);
assert.equal("type" in chatModel, false);
assert.equal("supported_endpoints" in chatModel, false);
assert.equal(chatModel.context_length, 8192);
});
test("v1 models catalog includes synced non-Gemini provider models from discovery cache", async () => {
const connection = await seedConnection("opencode-go", {
name: "opencode-go-synced",
apiKey: "go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", (connection as any).id, [
{
id: "glm-5.1",
name: "GLM 5.1",
source: "imported",
supportedEndpoints: ["chat"],
inputTokenLimit: 262144,
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const syncedModel = body.data.find((item) => item.id === "opencode-go/glm-5.1");
assert.equal(response.status, 200);
assert.ok(syncedModel);
assert.equal(syncedModel.owned_by, "opencode-go");
assert.equal(syncedModel.context_length, 262144);
});
test("v1 models catalog retains registered effort aliases beside synced OpenCode Go bases", async () => {
const connection = await seedConnection("opencode-go", {
name: "opencode-go-effort-aliases",
apiKey: "go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
{
id: "hy3",
name: "Hunyuan3",
source: "imported",
supportedEndpoints: ["chat"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as { data: Array<{ id: string }> };
const ids = body.data.map((item: { id: string }) => item.id);
assert.equal(response.status, 200);
assert.ok(ids.includes("opencode-go/hy3"));
assert.ok(ids.includes("opencode-go/hy3-none"));
assert.ok(ids.includes("opencode-go/hy3-low"));
assert.ok(ids.includes("opencode-go/hy3-high"));
});
test("v1 models catalog advertises GLM-5.2 provider aliases with hosted context limits", async () => {
const hfConnection = await seedConnection("huggingface", {
name: "huggingface-glm52",
apiKey: "hf-key",
});
const cfConnection = await seedConnection("cloudflare-ai", {
name: "cloudflare-glm52",
apiKey: "cf-key",
});
const zenmuxConnection = await seedConnection("zenmux", {
name: "zenmux-glm52",
apiKey: "zen-key",
});
await seedConnection("opencode-go", {
name: "opencode-go-glm52",
apiKey: "go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection(
"huggingface",
(hfConnection as any).id,
[
{
id: "zai-org/GLM-5.2",
name: "GLM 5.2",
source: "imported",
supportedEndpoints: ["chat"],
inputTokenLimit: 128000,
outputTokenLimit: 128000,
},
]
);
await modelsDb.replaceSyncedAvailableModelsForConnection(
"cloudflare-ai",
(cfConnection as any).id,
[
{
id: "@cf/zai-org/glm-5.2",
name: "GLM 5.2",
source: "imported",
supportedEndpoints: ["chat"],
inputTokenLimit: 128000,
outputTokenLimit: 128000,
},
]
);
await modelsDb.replaceSyncedAvailableModelsForConnection("zenmux", (zenmuxConnection as any).id, [
{
id: "z-ai/glm-5.2",
name: "GLM 5.2",
source: "imported",
supportedEndpoints: ["chat"],
inputTokenLimit: 128000,
outputTokenLimit: 128000,
},
]);
try {
modelsDevSync.saveModelsDevCapabilities({
huggingface: {
"zai-org/GLM-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
},
"cloudflare-ai": {
"@cf/zai-org/glm-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
},
zenmux: {
"z-ai/glm-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
},
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const byId = new Map(body.data.map((item) => [item.id, item]));
for (const [id, expectedContext] of [
["huggingface/zai-org/GLM-5.2", 128000],
["cloudflare-ai/@cf/zai-org/glm-5.2", 128000],
["opencode-go/glm-5.2", 1000000],
["zenmux/z-ai/glm-5.2", 128000],
] as const) {
const model = byId.get(id) as any;
assert.ok(model, `expected ${id} in catalog`);
assert.equal(model.context_length, expectedContext, id);
assert.equal(model.max_input_tokens, expectedContext, id);
}
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog includes media, moderation, rerank, video, and music models for active providers", async () => {
await seedConnection("openai", { name: "openai-media" });
await seedConnection("cohere", { name: "cohere-rerank" });
await seedConnection("comfyui", {
name: "comfy-media",
apiKey: null,
accessToken: null,
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const byId = new Map(body.data.map((item) => [item.id, item]));
assert.equal(response.status, 200);
assert.equal((byId.get("openai/gpt-image-2") as any).type, "image");
assert.equal((byId.get("openai/whisper-1") as any).type, "audio");
assert.equal((byId.get("openai/whisper-1") as any).subtype, "transcription");
assert.equal((byId.get("openai/omni-moderation-latest") as any).type, "moderation");
assert.equal((byId.get("cohere/rerank-v3.5") as any).type, "rerank");
assert.equal((byId.get("comfyui/animatediff") as any).type, "video");
assert.equal((byId.get("comfyui/stable-audio-open") as any).type, "music");
});
test("v1 models catalog does not duplicate imported Jina specialty models", async () => {
const connection = await seedConnection("jina-ai", {
name: "jina-synced",
apiKey: "jina-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("jina-ai", (connection as any).id, [
{
id: "jina-embeddings-v5-text-small",
name: "Jina Embeddings v5 Text Small",
source: "imported",
apiFormat: "embeddings",
supportedEndpoints: ["embeddings"],
},
{
id: "jina-reranker-v3",
name: "Jina Reranker v3",
source: "imported",
apiFormat: "rerank",
supportedEndpoints: ["rerank"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const visibleJinaEmbeddingRows = body.data.filter(
(item) =>
item.owned_by === "jina-ai" &&
item.root === "jina-embeddings-v5-text-small" &&
item.type === "embedding" &&
!item.parent
);
const visibleJinaRerankRows = body.data.filter(
(item) =>
item.owned_by === "jina-ai" &&
item.root === "jina-reranker-v3" &&
item.type === "rerank" &&
!item.parent
);
assert.equal(response.status, 200);
assert.equal(visibleJinaEmbeddingRows.length, 1);
assert.equal(visibleJinaEmbeddingRows[0].id, "jina/jina-embeddings-v5-text-small");
assert.equal(visibleJinaRerankRows.length, 1);
assert.equal(visibleJinaRerankRows[0].id, "jina/jina-reranker-v3");
});
test("v1 models catalog does not duplicate custom Jina specialty models", async () => {
await seedConnection("jina-ai", {
name: "jina-custom",
apiKey: "jina-key",
});
await modelsDb.addCustomModel(
"jina-ai",
"jina-embeddings-v5-text-small",
"Jina Embeddings v5 Text Small",
"imported",
"embeddings",
["embeddings"]
);
await modelsDb.addCustomModel(
"jina-ai",
"jina-reranker-v3",
"Jina Reranker v3",
"imported",
"rerank",
["rerank"]
);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const visibleJinaEmbeddingRows = body.data.filter(
(item) =>
item.owned_by === "jina-ai" &&
item.root === "jina-embeddings-v5-text-small" &&
item.type === "embedding" &&
!item.parent
);
const visibleJinaRerankRows = body.data.filter(
(item) =>
item.owned_by === "jina-ai" &&
item.root === "jina-reranker-v3" &&
item.type === "rerank" &&
!item.parent
);
assert.equal(response.status, 200);
assert.equal(visibleJinaEmbeddingRows.length, 1);
assert.equal(visibleJinaEmbeddingRows[0].id, "jina-ai/jina-embeddings-v5-text-small");
assert.equal(visibleJinaRerankRows.length, 1);
assert.equal(visibleJinaRerankRows[0].id, "jina-ai/jina-reranker-v3");
});
test("v1 models catalog exposes image model input and output modalities for advanced image providers", async () => {
await seedConnection("together", { name: "together-images" });
await seedConnection("topaz", { name: "topaz-images" });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const byId = new Map(body.data.map((item) => [item.id, item]));
assert.equal(response.status, 200);
assert.deepEqual((byId as any).get("flux-2-dev")?.input_modalities, ["text", "image"]);
(assert as any).deepEqual((byId.get("flux-2-dev") as any).output_modalities, ["image"]);
(assert as any).equal((byId.get("flux-2-dev") as any).type, "image");
assert.ok((byId.get("flux-2-dev") as any).supported_sizes?.includes("1024x1024"));
(assert as any).deepEqual((byId.get("topaz/topaz-enhance") as any).input_modalities, ["image"]);
assert.deepEqual((byId.get("topaz/topaz-enhance") as any).output_modalities, ["image"]);
});
test("v1 models catalog tolerates custom model lookup failures and keeps builtin models available", async () => {
await seedConnection("openai", { name: "openai-custom-failure" });
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
const originalLog = console.log;
const logs = [];
db.prepare = (sql) => {
if (String(sql) === "SELECT key, value FROM key_value WHERE namespace = 'customModels'") {
throw new Error("custom models offline");
}
return originalPrepare(sql);
};
console.log = (...args) => {
logs.push(args.map((arg) => String(arg)).join(" "));
};
try {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.ok(body.data.some((item) => item.id === "openai/gpt-4o-2024-11-20"));
assert.ok(logs.some((entry) => entry.includes("Could not fetch custom models")));
} finally {
db.prepare = originalPrepare;
console.log = originalLog;
}
});
test("v1 models catalog exposes provider-prefixed custom models, filters by raw model permissions, and skips hidden or Gemini custom rows", async () => {
await seedConnection("cline", {
authType: "oauth",
name: "cline-custom",
apiKey: null,
accessToken: "cline-access",
});
await seedConnection("gemini", { name: "gemini-custom" });
await modelsDb.addCustomModel("cline", "demo-custom", "Demo Custom", "manual", "responses", [
"images",
]);
await modelsDb.updateCustomModel("cline", "demo-custom", {
inputTokenLimit: 1234,
});
await modelsDb.addCustomModel("gemini", "gemini-custom-only", "Gemini Custom");
const db = core.getDbInstance();
db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'customModels' AND key = ?").run(
JSON.stringify([
{
id: "demo-custom",
name: "Demo Custom",
apiFormat: "responses",
supportedEndpoints: ["images"],
inputTokenLimit: 1234,
},
{
id: "hidden-custom",
name: "Hidden Custom",
isHidden: true,
},
{
name: "Missing Id",
},
null,
]),
"cline"
);
const key = await apiKeysDb.createApiKey("catalog-root-filter", "machine-root-filter");
await apiKeysDb.updateApiKeyPermissions(key.id, {
allowedModels: ["demo-custom"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models", {
headers: { Authorization: `Bearer ${key.key}` },
})
);
const body = (await response.json()) as any;
const ids = new Set(body.data.map((item) => item.id));
const shortAlias = body.data.find((item) => item.id === "cl/demo-custom");
const providerAlias = body.data.find((item) => item.id === "cline/demo-custom");
assert.equal(response.status, 200);
assert.ok(ids.has("cl/demo-custom"));
assert.ok(ids.has("cline/demo-custom"));
assert.equal(ids.has("cl/hidden-custom"), false);
assert.equal(ids.has("gemini/gemini-custom-only"), false);
assert.equal(shortAlias.type, "image");
assert.equal(shortAlias.api_format, "responses");
assert.deepEqual(shortAlias.supported_endpoints, ["images"]);
assert.equal(shortAlias.context_length, 1234);
assert.equal(providerAlias.parent, "cl/demo-custom");
});
test("v1 models catalog uses synced models.dev limits instead of provider defaults", async () => {
await seedConnection("openai", { name: "openai-models-dev" });
try {
modelsDevSync.saveModelsDevCapabilities({
openai: {
"gpt-5.5": {
tool_call: true,
reasoning: true,
attachment: true,
structured_output: true,
temperature: true,
modalities_input: JSON.stringify(["text", "image"]),
modalities_output: JSON.stringify(["text"]),
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: "gpt-5",
open_weights: false,
limit_context: 1050000,
limit_input: 1050000,
limit_output: 128000,
interleaved_field: null,
},
},
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const model = body.data.find((item) => item.id === "openai/gpt-5.5");
assert.equal(response.status, 200);
assert.ok(model);
assert.equal(model.context_length, 1050000);
assert.equal(model.max_input_tokens, 1050000);
assert.equal(model.max_output_tokens, 128000);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog exposes Bedrock Claude token limits from static metadata", async () => {
await seedConnection("bedrock", { name: "bedrock-limits" });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const sonnet46 = body.data.find((item) => item.id === "bedrock/anthropic.claude-sonnet-4-6");
const sonnet45 = body.data.find((item) => item.id === "bedrock/anthropic.claude-sonnet-4-5");
const opus46 = body.data.find((item) => item.id === "bedrock/anthropic.claude-opus-4-6");
assert.equal(response.status, 200);
assert.ok(sonnet46);
assert.equal(sonnet46.context_length, 1000000);
assert.equal(sonnet46.max_input_tokens, 1000000);
assert.equal(sonnet46.max_output_tokens, 64000);
assert.ok(sonnet45);
assert.equal(sonnet45.context_length, 200000);
assert.equal(sonnet45.max_output_tokens, 64000);
assert.ok(opus46);
assert.equal(opus46.context_length, 1000000);
assert.equal(opus46.max_output_tokens, 128000);
});
test("v1 models catalog lets provider-specific synced limits beat global static specs", async () => {
await seedConnection("github", {
authType: "oauth",
name: "github-copilot-models-dev",
apiKey: null,
accessToken: "github-access",
});
try {
modelsDevSync.saveModelsDevCapabilities({
github: {
"gpt-5.5": {
tool_call: true,
reasoning: true,
attachment: true,
structured_output: true,
temperature: true,
modalities_input: JSON.stringify(["text", "image"]),
modalities_output: JSON.stringify(["text"]),
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: "gpt-5",
open_weights: false,
limit_context: 400000,
limit_input: 272000,
limit_output: 128000,
interleaved_field: null,
},
},
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const model = body.data.find((item) => item.id === "gh/gpt-5.5");
assert.equal(response.status, 200);
assert.ok(model);
assert.equal(model.context_length, 400000);
assert.equal(model.max_input_tokens, 272000);
assert.equal(model.max_output_tokens, 128000);
} finally {
modelsDevSync.saveModelsDevCapabilities({});
}
});
test("v1 models catalog returns 500 when model compatibility lookup crashes", async () => {
await seedConnection("openai", { name: "openai-compat-crash" });
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
const originalLog = console.log;
const logs = [];
db.prepare = (sql) => {
const statement = originalPrepare(sql);
// #9147: the catalog builder now resolves hidden models via a single bulk
// read (`getHiddenModelsByProvider()`, src/lib/db/models.ts) instead of the
// old per-provider `SELECT value FROM key_value WHERE namespace = ? AND
// key = ?` / readCompatList() lookup — intercept the bulk query's `.all()`
// call so this test still exercises "DB read for model visibility crashes
// -> catalog endpoint surfaces 500" against the current implementation.
if (
String(sql) !==
"SELECT namespace, key, value FROM key_value WHERE namespace IN ('modelCompatOverrides', 'customModels')"
) {
return statement;
}
return new Proxy(statement, {
get(target, prop, receiver) {
if (prop === "all") {
return (...args) => {
throw new Error("compat lookup boom");
};
}
return Reflect.get(target, prop, receiver);
},
});
};
console.log = (...args) => {
logs.push(args.map((arg) => String(arg)).join(" "));
};
try {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
assert.equal(response.status, 500);
assert.equal(body.error.type, "server_error");
assert.match(body.error.message, /compat lookup boom/i);
assert.ok(logs.some((entry) => entry.includes("Error fetching models:")));
} finally {
db.prepare = originalPrepare;
console.log = originalLog;
}
});
test("v1 models catalog skips duplicate built-ins and custom models from inactive providers", async () => {
await seedConnection("openai", { name: "openai-duplicate" });
await seedConnection("cline", {
authType: "oauth",
name: "cline-inactive-custom",
apiKey: null,
accessToken: "cline-access",
isActive: false,
});
await modelsDb.addCustomModel("openai", "gpt-4o-2024-11-20", "Duplicate Builtin");
await modelsDb.addCustomModel("cline", "inactive-only", "Inactive Only");
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const duplicateBuiltins = body.data.filter((item) => item.id === "openai/gpt-4o-2024-11-20");
assert.equal(response.status, 200);
// Still exactly one entry: the custom row overlays the built-in, it does not duplicate it.
assert.equal(duplicateBuiltins.length, 1);
// #10248 changed the contract: a custom row for an id that already exists is the
// operator-owned overlay for that model (catalog.ts:1330) — its explicitly stored
// fields win over the discovered metadata, and the merged entry is flagged `custom`.
// Before #10248 the duplicate was skipped outright, so this asserted `false`.
assert.equal(duplicateBuiltins[0].custom, true);
// The overlay must keep the catalog identity rather than becoming a detached entry.
assert.equal(duplicateBuiltins[0].id, "openai/gpt-4o-2024-11-20");
assert.equal(
body.data.some((item) => item.id === "cl/inactive-only" || item.id === "cline/inactive-only"),
false
);
});
test("v1 models catalog adds managed fallback models for Claude-compatible providers", async () => {
await providersDb.createProviderNode({
id: "anthropic-compatible-cc-demo",
type: "anthropic-compatible",
name: "Claude Compatible Demo",
prefix: "ccdemo",
baseUrl: "https://proxy.example.com",
chatPath: "/v1/messages",
modelsPath: "/v1/models",
});
await seedConnection("anthropic-compatible-cc-demo", {
name: "claude-compatible-node",
providerSpecificData: {
baseUrl: "https://proxy.example.com",
chatPath: "/v1/messages",
modelsPath: "/v1/models",
},
});
modelsDb.mergeModelCompatOverride("anthropic-compatible-cc-demo", "claude-sonnet-4-6", {
isHidden: true,
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids = new Set(body.data.map((item) => item.id));
assert.equal(response.status, 200);
assert.ok(ids.has("ccdemo/claude-opus-4-7"));
assert.ok(ids.has("ccdemo/claude-opus-4-6"));
assert.equal(ids.has("ccdemo/claude-sonnet-4-6"), false);
});
test("v1 models catalog auto-calculates combo context_length from targets when not set manually", async () => {
await seedConnection("openai", { name: "openai-auto-context" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-auto-context",
apiKey: null,
accessToken: "claude-access",
});
// Create a combo with targets having different context limits.
// openai/gpt-4o context = 128000, claude/claude-sonnet-4-6 = 1000000 (#7129: 1M GA).
// The combo should expose context_length = min = 128000.
const combo = await combosDb.createCombo({
name: "auto-context-combo",
strategy: "priority",
models: ["openai/gpt-4o", "claude/claude-sonnet-4-6"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const comboModel = body.data.find((item) => item.id === "auto-context-combo");
assert.equal(response.status, 200);
assert.ok(comboModel);
assert.equal(
comboModel.context_length,
128000,
"combo context_length should be the MIN of all target model limits"
);
});
test("v1 models catalog includes context_length for individual chat models", async () => {
await seedConnection("openai", { name: "openai-context" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-context",
apiKey: null,
accessToken: "claude-access",
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
// Individual chat models only — combos/routers (owned_by "combo", incl. the
// built-in auto/* entries from #4164) resolve dynamically and have no fixed
// context_length, so they are not "individual chat models" for this check.
const chatModels = body.data.filter(
(item) => (!item.type || item.type === "chat") && item.owned_by !== "combo"
);
assert.equal(response.status, 200);
assert.ok(chatModels.length > 0, "should have at least one chat model");
for (const model of chatModels) {
assert.ok(
typeof model.context_length === "number" && model.context_length > 0,
`chat model ${model.id} should have a positive context_length, got ${model.context_length}`
);
}
});
test("v1 models catalog falls back to getTokenLimit for models without registry defaultContextLength", async () => {
// opencode-go has defaultContextLength in REGISTRY, but we test the fallback
// path by verifying models from the synced path still get context_length
const connection = await seedConnection("opencode-go", {
name: "opencode-go-context-fallback",
apiKey: "go-key",
});
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", (connection as any).id, [
{
id: "test-model-no-context",
name: "Test Model No Context",
source: "imported",
supportedEndpoints: ["chat"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const model = body.data.find((item) => item.id === "opencode-go/test-model-no-context");
assert.equal(response.status, 200);
assert.ok(model, "synced model should appear");
assert.ok(
typeof model.context_length === "number" && model.context_length > 0,
`synced model without inputTokenLimit should get context_length via getTokenLimit fallback, got ${model.context_length}`
);
});
test("v1 models catalog prefers manual combo context_length over auto-calculated", async () => {
await seedConnection("openai", { name: "openai-manual-context" });
const combo = await combosDb.createCombo({
name: "manual-context-combo",
strategy: "priority",
models: ["openai/gpt-4o"],
});
await combosDb.updateCombo((combo as any).id, { context_length: 64000 });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const comboModel = body.data.find((item) => item.id === "manual-context-combo");
assert.equal(response.status, 200);
assert.ok(comboModel);
assert.equal(comboModel.context_length, 64000, "manual context_length should override auto-calc");
});
test("v1 models catalog computes combo context_length from known targets when some targets have unknown context", async () => {
await seedConnection("openai", { name: "openai-mixed-context" });
await seedConnection("claude", {
authType: "oauth",
name: "claude-mixed-context",
apiKey: null,
accessToken: "claude-access",
});
// Create a combo with targets: one known (gpt-4o = 128K), one unknown (nonexistent-model).
// The combo should still compute context_length = 128K from the known target.
const combo = await combosDb.createCombo({
name: "mixed-context-combo",
strategy: "priority",
models: ["openai/gpt-4o", "openai/nonexistent-model-xyz"],
});
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const comboModel = body.data.find((item) => item.id === "mixed-context-combo");
assert.equal(response.status, 200);
assert.ok(comboModel);
assert.equal(
comboModel.context_length,
128000,
"combo context_length should be the MIN of known target model limits, ignoring targets with no registry/spec/synced source"
);
});
// Regression test for Issue #2798: noAuth providers (opencode/oc) have no DB connection rows
// but their models must still appear in /v1/models.
test("v1 models catalog includes noAuth provider models when no DB connections exist (#2798)", async () => {
// No connections seeded — empty DB, simulating a fresh install with no credentials added.
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids: string[] = body.data.map((item: any) => item.id);
assert.equal(response.status, 200);
// opencode (noAuth) models must surface even with zero connection rows.
// The registry defines models under alias "oc" (e.g. "oc/big-pickle").
assert.ok(
ids.some((id) => id.startsWith("oc/")),
`Expected at least one oc/* model in /v1/models but got none. IDs sample: ${ids.slice(0, 10).join(", ")}`
);
assert.equal(
ids.some((id) => id.startsWith("opencode/")),
false,
"catalog must not return opencode/* noAuth aliases because opencode/ routes to opencode-zen"
);
});
test("v1 models catalog hides disabled noAuth provider models", async () => {
await settingsDb.updateSettings({ blockedProviders: ["opencode", "duckduckgo-web"] });
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as any;
const ids: string[] = body.data.map((item: any) => item.id);
assert.equal(response.status, 200);
assert.equal(
ids.some((id) => id.startsWith("oc/")),
false,
"OpenCode no-auth models must be hidden while no-auth providers are disabled"
);
assert.equal(
ids.some((id) => id.startsWith("ddgw/")),
false,
"DuckDuckGo no-auth models must be hidden while no-auth providers are disabled"
);
});