From 065301f79dd8cac3628b26cf8d78f2cf3cbad324 Mon Sep 17 00:00:00 2001 From: 3g0r1ch Date: Thu, 6 Aug 2026 06:09:18 +0300 Subject: [PATCH] feat(model-alias): add runtime Model Alias Resolver middleware (#9020) Validated in local merge-train T5 (base49+contributors+pacocartones) --- src/app/api/v1/chat/completions/route.ts | 14 ++++- src/lib/modelAliasResolver.ts | 74 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/lib/modelAliasResolver.ts diff --git a/src/app/api/v1/chat/completions/route.ts b/src/app/api/v1/chat/completions/route.ts index 77eed06416..4c8f16331a 100644 --- a/src/app/api/v1/chat/completions/route.ts +++ b/src/app/api/v1/chat/completions/route.ts @@ -24,6 +24,7 @@ import { readCompressionRequestHeader, withCompressionHeaderEcho, } from "@/shared/utils/compressionHeaderEcho"; +import { resolveModelAliasOnBody } from "@/lib/modelAliasResolver"; let initPromise = null; @@ -135,14 +136,23 @@ export async function POST(request) { if (!shapeCheck.success) { const issue = shapeCheck.error.issues[0]; const field = issue?.path?.length ? issue.path.join(".") : "body"; - return finishAdmission(errorResponse(400, `${field}: ${issue?.message ?? "Invalid request"}`)); + return finishAdmission( + errorResponse(400, `${field}: ${issue?.message ?? "Invalid request"}`) + ); } } const structuralAdmission = admitChatStructure(parsedBody, admission.lease); if (structuralAdmission.admit === false) { admission.lease?.release(); - return structuralAdmission.response; + return finishAdmission(structuralAdmission.response); + } + + // Resolve model alias before forwarding to handleChat + if (parsedBody && typeof parsedBody === "object") { + await resolveModelAliasOnBody(parsedBody).catch(() => { + /* swallow — fall through with original model */ + }); } admission.lease = structuralAdmission.lease; diff --git a/src/lib/modelAliasResolver.ts b/src/lib/modelAliasResolver.ts new file mode 100644 index 0000000000..8872019e15 --- /dev/null +++ b/src/lib/modelAliasResolver.ts @@ -0,0 +1,74 @@ +/** + * Model Alias Resolver — maps client-facing model names to OmniRoute provider IDs. + * + * When a client sends `model: "deepseek-chat"`, this resolver looks up the alias + * in the database and rewrites it to the target model ID (e.g. `"ds/deepseek-v4-flash"`) + * before the request reaches the chat handler. + * + * Aliases are stored in the `modelAliases` key-value namespace and seeded by + * `src/lib/modelAliasSeed.ts`. + */ +import { getModelAliases } from "@/lib/db/models/aliases"; + +let cachedAliases: Record | null = null; +let lastFetch = 0; +const CACHE_TTL_MS = 60_000; // 1 minute + +async function loadAliases(): Promise> { + const now = Date.now(); + if (cachedAliases && now - lastFetch < CACHE_TTL_MS) { + return cachedAliases; + } + cachedAliases = await getModelAliases(); + lastFetch = now; + return cachedAliases; +} + +/** + * Resolve a model alias to its target provider model ID. + * If the alias maps to an array, returns the first element. + * If no alias is found, returns the original model name unchanged. + */ +export async function resolveModelAlias( + model: string | null | undefined +): Promise { + if (!model) return model; + + const aliases = await loadAliases(); + const target = aliases[model]; + + if (target === undefined) return model; + + if (typeof target === "string") return target; + + if (Array.isArray(target) && target.length > 0) { + const first = target[0]; + return typeof first === "string" ? first : model; + } + + if (typeof target === "object" && target !== null) { + const t = target as { provider?: string; model?: string }; + if (t.provider && t.model) return `${t.provider}/${t.model}`; + } + + return model; +} + +/** + * Resolve model alias on a parsed request body in-place. + * Mutates `body.model` if an alias is found. + */ +export async function resolveModelAliasOnBody( + body: Record | null | undefined +): Promise { + if (!body || typeof body !== "object") return; + body.model = await resolveModelAlias(body.model as string | null | undefined); +} + +/** + * Invalidate the alias cache (e.g. after a new alias is added). + */ +export function invalidateAliasCache(): void { + cachedAliases = null; + lastFetch = 0; +}