fix openrouter available models sync

This commit is contained in:
R.D.
2026-03-30 17:05:19 -04:00
parent df23162e9d
commit ff73de5716
8 changed files with 363 additions and 66 deletions
+2
View File
@@ -1541,6 +1541,8 @@ Models:
**Models:** Access 100+ models from all major providers through a single API key.
**Dashboard behavior:** OpenRouter models are managed from **Available Models**. Manual add, import, and auto-sync all update the same list.
</details>
<details>
+19 -19
View File
@@ -686,25 +686,25 @@ Additional processing layers in the translation pipeline:
## Supported API Endpoints
| Endpoint | Format | Handler |
| -------------------------------------------------- | ------------------ | ---------------------------------------------------- |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
| `GET /v1/embeddings` | Model listing | API route |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
| `GET /v1/images/generations` | Model listing | API route |
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation |
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation |
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation |
| `POST /v1/messages/count_tokens` | Claude Token Count | API route |
| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) |
| `GET /api/models/catalog` | Catalog | All models grouped by provider + type |
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route |
| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration |
| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint |
| `GET/POST/DELETE /api/provider-models` | Custom Models | Custom model management per provider |
| Endpoint | Format | Handler |
| -------------------------------------------------- | ------------------ | ------------------------------------------------------------------- |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
| `GET /v1/embeddings` | Model listing | API route |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
| `GET /v1/images/generations` | Model listing | API route |
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation |
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation |
| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation |
| `POST /v1/messages/count_tokens` | Claude Token Count | API route |
| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) |
| `GET /api/models/catalog` | Catalog | All models grouped by provider + type |
| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route |
| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration |
| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint |
| `GET/POST/DELETE /api/provider-models` | Provider Models | Provider model metadata backing custom and managed available models |
## Bypass Handler
+5
View File
@@ -596,6 +596,11 @@ curl -X POST http://localhost:20128/api/provider-models \
Or use Dashboard: **Providers → [Provider] → Custom Models**.
Notes:
- OpenRouter and OpenAI/Anthropic-compatible providers are managed from **Available Models** only. Manual add, import, and auto-sync all land in the same available-model list, so there is no separate Custom Models section for those providers.
- The **Custom Models** section is intended for providers that do not expose managed available-model imports.
### Dedicated Provider Routes
Route requests directly to a specific provider with model validation:
@@ -36,6 +36,7 @@ import {
MODEL_COMPAT_PROTOCOL_KEYS,
type ModelCompatProtocolKey,
} from "@/shared/constants/modelCompat";
import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases";
type CompatByProtocolMap = Partial<
Record<
@@ -331,6 +332,10 @@ interface CompatibleModelsSectionProps {
providerStorageAlias: string;
providerDisplayAlias: string;
modelAliases: Record<string, string>;
fallbackModels?: CompatModelRow[];
description: string;
inputLabel: string;
inputPlaceholder: string;
copied?: string;
onCopy: (text: string, key: string) => void;
onSetAlias: (modelId: string, alias: string, providerStorageAlias?: string) => Promise<void>;
@@ -850,6 +855,7 @@ export default function ProviderDetailPage() {
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
const isCompatible = isOpenAICompatible || isAnthropicCompatible;
const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter";
const isSearchProvider = providerId.endsWith("-search");
const providerStorageAlias = isCompatible ? providerId : providerAlias;
@@ -1469,7 +1475,10 @@ export default function ProviderDetailPage() {
logs: [
t("foundModelsStartingImport", { count: newModels.length }),
...(newModels.length < fetchedModels.length
? [t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) || `Skipping ${fetchedModels.length - newModels.length} existing models`]
? [
t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) ||
`Skipping ${fetchedModels.length - newModels.length} existing models`,
]
: []),
],
}));
@@ -1664,6 +1673,14 @@ export default function ProviderDetailPage() {
};
const [clearingModels, setClearingModels] = useState(false);
const providerAliasEntries = useMemo(
() =>
Object.entries(modelAliases).filter(([, model]) =>
(model as string).startsWith(`${providerStorageAlias}/`)
),
[modelAliases, providerStorageAlias]
);
const handleClearAllModels = async () => {
if (clearingModels) return;
if (!confirm(t("clearAllModelsConfirm"))) return;
@@ -1675,11 +1692,8 @@ export default function ProviderDetailPage() {
);
if (res.ok) {
// Also delete all aliases that belong to this provider
const aliasEntries = Object.entries(modelAliases).filter(([, model]) =>
(model as string).startsWith(`${providerStorageAlias}/`)
);
await Promise.all(
aliasEntries.map(([alias]) =>
providerAliasEntries.map(([alias]) =>
fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, {
method: "DELETE",
}).catch(() => {})
@@ -1806,7 +1820,8 @@ export default function ProviderDetailPage() {
</button>
);
const clearAllButton = modelMeta.customModels.length > 0 && (
const clearAllButton = (modelMeta.customModels.length > 0 ||
providerAliasEntries.length > 0) && (
<button
onClick={handleClearAllModels}
disabled={clearingModels}
@@ -1818,7 +1833,21 @@ export default function ProviderDetailPage() {
</button>
);
if (isCompatible) {
if (isManagedAvailableModelsProvider) {
const description =
providerId === "openrouter"
? t("openRouterAnyModelHint")
: t("compatibleModelsDescription", {
type: isAnthropicCompatible ? t("anthropic") : t("openai"),
});
const inputLabel = providerId === "openrouter" ? t("modelIdFromOpenRouter") : t("modelId");
const inputPlaceholder =
providerId === "openrouter"
? t("openRouterModelPlaceholder")
: isAnthropicCompatible
? t("anthropicCompatibleModelPlaceholder")
: t("openaiCompatibleModelPlaceholder");
return (
<div>
<div className="flex items-center gap-2 mb-4">
@@ -1829,6 +1858,10 @@ export default function ProviderDetailPage() {
providerStorageAlias={providerStorageAlias}
providerDisplayAlias={providerDisplayAlias}
modelAliases={modelAliases}
fallbackModels={providerId === "openrouter" ? modelMeta.customModels : undefined}
description={description}
inputLabel={inputLabel}
inputPlaceholder={inputPlaceholder}
copied={copied}
onCopy={copy}
onSetAlias={handleSetAlias}
@@ -2367,8 +2400,8 @@ export default function ProviderDetailPage() {
<h2 className="text-lg font-semibold mb-4">{t("availableModels")}</h2>
{renderModelsSection()}
{/* Custom Models — available for non-compatible, non-search providers */}
{!isCompatible && (
{/* Custom Models — available for providers without managed available-model metadata */}
{!isManagedAvailableModelsProvider && (
<CustomModelsSection
providerId={providerId}
providerAlias={providerDisplayAlias}
@@ -3410,6 +3443,10 @@ function CompatibleModelsSection({
providerStorageAlias,
providerDisplayAlias,
modelAliases,
fallbackModels = [],
description,
inputLabel,
inputPlaceholder,
copied,
onCopy,
onSetAlias,
@@ -3430,33 +3467,45 @@ function CompatibleModelsSection({
const [importing, setImporting] = useState(false);
const notify = useNotificationStore();
const providerAliases = Object.entries(modelAliases).filter(([, model]: [string, any]) =>
(model as string).startsWith(`${providerStorageAlias}/`)
const providerAliases = useMemo(
() =>
Object.entries(modelAliases).filter(([, model]: [string, any]) =>
(model as string).startsWith(`${providerStorageAlias}/`)
),
[modelAliases, providerStorageAlias]
);
const allModels = providerAliases.map(([alias, fullModel]: [string, any]) => ({
modelId: (fullModel as string).replace(`${providerStorageAlias}/`, ""),
fullModel,
alias,
}));
const allModels = useMemo(() => {
const rows = providerAliases.map(([alias, fullModel]: [string, any]) => ({
modelId: (fullModel as string).replace(`${providerStorageAlias}/`, ""),
alias,
}));
const generateDefaultAlias = (modelId) => {
const parts = modelId.split("/");
return parts[parts.length - 1];
};
const seenModelIds = new Set(rows.map((row) => row.modelId));
for (const model of fallbackModels) {
if (!model?.id || seenModelIds.has(model.id)) continue;
rows.push({ modelId: model.id, alias: null });
seenModelIds.add(model.id);
}
const resolveAlias = (modelId) => {
const baseAlias = generateDefaultAlias(modelId);
if (!modelAliases[baseAlias]) return baseAlias;
const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`;
if (!modelAliases[prefixedAlias]) return prefixedAlias;
return null;
};
return rows;
}, [fallbackModels, providerAliases, providerStorageAlias]);
const resolveAlias = useCallback(
(modelId: string, workingAliases: Record<string, string>) =>
resolveManagedModelAlias({
modelId,
fullModel: `${providerStorageAlias}/${modelId}`,
providerDisplayAlias,
existingAliases: workingAliases,
}),
[providerDisplayAlias, providerStorageAlias]
);
const handleAdd = async () => {
if (!newModel.trim() || adding) return;
const modelId = newModel.trim();
const resolvedAlias = resolveAlias(modelId);
const resolvedAlias = resolveAlias(modelId, modelAliases);
if (!resolvedAlias) {
notify.error(t("allSuggestedAliasesExist"));
return;
@@ -3506,6 +3555,7 @@ function CompatibleModelsSection({
setImporting(true);
try {
const workingAliases = { ...modelAliases };
await onImportWithProgress(
// fetchModels callback
async () => {
@@ -3518,7 +3568,7 @@ function CompatibleModelsSection({
async (model: any) => {
const modelId = model.id || model.name || model.model;
if (!modelId) return false;
const resolvedAlias = resolveAlias(modelId);
const resolvedAlias = resolveAlias(modelId, workingAliases);
if (!resolvedAlias) return false;
// Save to customModels DB FIRST - only create alias if this succeeds
@@ -3540,6 +3590,7 @@ function CompatibleModelsSection({
// Only create alias after customModel is saved successfully
await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
workingAliases[resolvedAlias] = `${providerStorageAlias}/${modelId}`;
return true;
}
);
@@ -3554,7 +3605,7 @@ function CompatibleModelsSection({
const canImport = connections.some((conn) => conn.isActive !== false);
// Handle delete: remove from both alias and customModels DB
const handleDeleteModel = async (modelId: string, alias: string) => {
const handleDeleteModel = async (modelId: string, alias?: string | null) => {
try {
// Remove from customModels DB
const res = await fetch(
@@ -3565,7 +3616,9 @@ function CompatibleModelsSection({
throw new Error(t("failedRemoveModelFromDatabase"));
}
// Also delete the alias
await onDeleteAlias(alias);
if (alias) {
await onDeleteAlias(alias);
}
notify.success(t("modelRemovedSuccess"));
onModelsChanged?.();
} catch (error) {
@@ -3576,11 +3629,7 @@ function CompatibleModelsSection({
return (
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">
{t("compatibleModelsDescription", {
type: isAnthropic ? t("anthropic") : t("openai"),
})}
</p>
<p className="text-sm text-text-muted">{description}</p>
<div className="flex items-end gap-2 flex-wrap">
<div className="flex-1 min-w-[240px]">
@@ -3588,7 +3637,7 @@ function CompatibleModelsSection({
htmlFor="new-compatible-model-input"
className="text-xs text-text-muted mb-1 block"
>
{t("modelId")}
{inputLabel}
</label>
<input
id="new-compatible-model-input"
@@ -3596,11 +3645,7 @@ function CompatibleModelsSection({
value={newModel}
onChange={(e) => setNewModel(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
placeholder={
isAnthropic
? t("anthropicCompatibleModelPlaceholder")
: t("openaiCompatibleModelPlaceholder")
}
placeholder={inputPlaceholder}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</div>
@@ -3622,9 +3667,9 @@ function CompatibleModelsSection({
{allModels.length > 0 && (
<div className="flex flex-col gap-3">
{allModels.map(({ modelId, fullModel, alias }) => (
{allModels.map(({ modelId, alias }) => (
<PassthroughModelRow
key={fullModel as string}
key={`${providerStorageAlias}:${modelId}`}
modelId={modelId}
fullModel={`${providerDisplayAlias}/${modelId}`}
copied={copied}
@@ -3649,6 +3694,10 @@ CompatibleModelsSection.propTypes = {
providerStorageAlias: PropTypes.string.isRequired,
providerDisplayAlias: PropTypes.string.isRequired,
modelAliases: PropTypes.object.isRequired,
fallbackModels: PropTypes.array,
description: PropTypes.string.isRequired,
inputLabel: PropTypes.string.isRequired,
inputPlaceholder: PropTypes.string.isRequired,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
onSetAlias: PropTypes.func.isRequired,
@@ -1,6 +1,10 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById } from "@/models";
import { replaceCustomModels } from "@/lib/db/models";
import {
syncManagedAvailableModelAliases,
usesManagedAvailableModels,
} from "@/lib/providerModels/managedAvailableModels";
import { saveCallLog } from "@/lib/usage/callLogs";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import {
@@ -77,9 +81,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const fetchedModels = modelsData.models || [];
// Filter out models already in the built-in registry
const registryIds = new Set(
getModelsByProviderId(connection.provider).map((m: any) => m.id)
);
const registryIds = new Set(getModelsByProviderId(connection.provider).map((m: any) => m.id));
// Replace the full model list
const models = fetchedModels
@@ -92,6 +94,15 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const replaced = await replaceCustomModels(connection.provider, models);
let syncedAliases = 0;
if (usesManagedAvailableModels(connection.provider)) {
const aliasSync = await syncManagedAvailableModelAliases(
connection.provider,
models.map((model: any) => model.id)
);
syncedAliases = aliasSync.assignedAliases.length;
}
// Log the successful sync
await saveCallLog({
method: "GET",
@@ -105,6 +116,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
requestType: "model-sync",
responseBody: {
syncedModels: models.length,
syncedAliases,
provider: connection.provider,
},
});
@@ -113,6 +125,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
ok: true,
provider: connection.provider,
syncedModels: replaced.length,
syncedAliases,
models: replaced,
});
} catch (error: any) {
@@ -0,0 +1,91 @@
import {
deleteModelAlias,
getModelAliases,
getProviderNodeById,
setModelAlias,
} from "@/lib/localDb";
import {
getProviderAlias,
isAnthropicCompatibleProvider,
isOpenAICompatibleProvider,
} from "@/shared/constants/providers";
import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases";
function isCompatibleProvider(providerId: string): boolean {
return isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
}
export function usesManagedAvailableModels(providerId: string): boolean {
return providerId === "openrouter" || isCompatibleProvider(providerId);
}
function getProviderStoragePrefix(providerId: string): string {
if (isCompatibleProvider(providerId)) return providerId;
return getProviderAlias(providerId) || providerId;
}
async function getProviderDisplayPrefix(providerId: string): Promise<string> {
if (!isCompatibleProvider(providerId)) {
return getProviderAlias(providerId) || providerId;
}
const providerNode = await getProviderNodeById(providerId);
const prefix = providerNode?.prefix;
return typeof prefix === "string" && prefix.trim().length > 0 ? prefix.trim() : providerId;
}
export async function syncManagedAvailableModelAliases(providerId: string, modelIds: string[]) {
const storagePrefix = getProviderStoragePrefix(providerId);
const displayPrefix = await getProviderDisplayPrefix(providerId);
const existingAliasesRaw = await getModelAliases();
const workingAliases = Object.fromEntries(
Object.entries(existingAliasesRaw).filter((entry): entry is [string, string] => {
const [, value] = entry;
return typeof value === "string";
})
);
const targetModelIds = Array.from(
new Set(
modelIds.map((modelId) => (typeof modelId === "string" ? modelId.trim() : "")).filter(Boolean)
)
);
const targetFullModels = new Set(targetModelIds.map((modelId) => `${storagePrefix}/${modelId}`));
const removedAliases: string[] = [];
for (const [alias, value] of Object.entries(workingAliases)) {
if (!value.startsWith(`${storagePrefix}/`)) continue;
if (targetFullModels.has(value)) continue;
await deleteModelAlias(alias);
delete workingAliases[alias];
removedAliases.push(alias);
}
const assignedAliases: string[] = [];
for (const modelId of targetModelIds) {
const fullModel = `${storagePrefix}/${modelId}`;
const alias = resolveManagedModelAlias({
modelId,
fullModel,
providerDisplayAlias: displayPrefix,
existingAliases: workingAliases,
});
if (!alias) continue;
if (workingAliases[alias] !== fullModel) {
await setModelAlias(alias, fullModel);
workingAliases[alias] = fullModel;
}
assignedAliases.push(alias);
}
return {
assignedAliases,
removedAliases,
storagePrefix,
};
}
+68
View File
@@ -0,0 +1,68 @@
type AliasMap = Record<string, string>;
export function getDefaultModelAliasBase(modelId: string): string {
const trimmed = modelId.trim();
if (!trimmed) return "";
const segments = trimmed
.split("/")
.map((segment) => segment.trim())
.filter(Boolean);
return segments[segments.length - 1] || trimmed;
}
export function resolveManagedModelAlias({
modelId,
fullModel,
providerDisplayAlias,
existingAliases,
}: {
modelId: string;
fullModel: string;
providerDisplayAlias: string;
existingAliases: AliasMap;
}): string | null {
const baseAlias = getDefaultModelAliasBase(modelId);
if (!baseAlias) return null;
for (const [alias, value] of Object.entries(existingAliases)) {
if (value === fullModel) return alias;
}
const displayAlias = providerDisplayAlias.trim();
const candidates: string[] = [];
const seen = new Set<string>();
const pushCandidate = (candidate: string) => {
const trimmed = candidate.trim();
if (!trimmed || seen.has(trimmed)) return;
seen.add(trimmed);
candidates.push(trimmed);
};
pushCandidate(baseAlias);
if (displayAlias) {
pushCandidate(`${displayAlias}-${baseAlias}`);
}
for (const candidate of candidates) {
if (!(candidate in existingAliases) || existingAliases[candidate] === fullModel) {
return candidate;
}
}
for (let suffix = 2; suffix <= 5000; suffix += 1) {
if (displayAlias) {
const prefixed = `${displayAlias}-${baseAlias}-${suffix}`;
if (!(prefixed in existingAliases) || existingAliases[prefixed] === fullModel) {
return prefixed;
}
}
const fallback = `${baseAlias}-${suffix}`;
if (!(fallback in existingAliases) || existingAliases[fallback] === fullModel) {
return fallback;
}
}
return null;
}
@@ -0,0 +1,69 @@
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-managed-models-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const managedModels = await import("../../src/lib/providerModels/managedAvailableModels.ts");
const aliasUtils = await import("../../src/shared/utils/providerModelAliases.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("resolveManagedModelAlias preserves existing aliases and falls back to provider-prefixed suffixes", () => {
const first = aliasUtils.resolveManagedModelAlias({
modelId: "anthropic/claude-3.7-sonnet",
fullModel: "openrouter/anthropic/claude-3.7-sonnet",
providerDisplayAlias: "openrouter",
existingAliases: {
"claude-3.7-sonnet": "other-provider/claude-3.7-sonnet",
"openrouter-claude-3.7-sonnet": "other-provider/claude-3.7-sonnet",
},
});
assert.equal(first, "openrouter-claude-3.7-sonnet-2");
const preserved = aliasUtils.resolveManagedModelAlias({
modelId: "openai/gpt-4.1",
fullModel: "openrouter/openai/gpt-4.1",
providerDisplayAlias: "openrouter",
existingAliases: {
kept: "openrouter/openai/gpt-4.1",
"gpt-4.1": "other-provider/gpt-4.1",
},
});
assert.equal(preserved, "kept");
});
test("syncManagedAvailableModelAliases backfills openrouter aliases and removes stale entries", async () => {
await modelsDb.setModelAlias("kept", "openrouter/openai/gpt-4.1");
await modelsDb.setModelAlias("claude-3.7-sonnet", "other-provider/claude-3.7-sonnet");
await modelsDb.setModelAlias("stale-model", "openrouter/legacy/stale-model");
const result = await managedModels.syncManagedAvailableModelAliases("openrouter", [
"openai/gpt-4.1",
"anthropic/claude-3.7-sonnet",
]);
const aliases = await modelsDb.getModelAliases();
assert.deepEqual(result.removedAliases, ["stale-model"]);
assert.equal(aliases.kept, "openrouter/openai/gpt-4.1");
assert.equal(aliases["openrouter-claude-3.7-sonnet"], "openrouter/anthropic/claude-3.7-sonnet");
assert.equal(aliases["stale-model"], undefined);
});