diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index d3b1210dcf..c0fd9582d6 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -496,6 +496,23 @@ export async function getModelInfoCore( // Get aliases (from object or function) const aliases = typeof aliasesOrGetter === "function" ? await aliasesOrGetter() : aliasesOrGetter; + // Local alias map (user-provided 2nd arg) wins over all cross-proxy / + // provider inference paths. When the alias target is a slashful string like + // "openai/gpt-4o", parse it directly as / and return + // immediately — before shouldTreatAsExactModelId() or cross-proxy inference + // can misclassify the target (e.g. because bazaarlink catalogs it verbatim). + if (aliases && parsed.model && typeof aliases[parsed.model] === "string") { + const directTarget = aliases[parsed.model]; + const slashIdx = directTarget.indexOf("/"); + if (slashIdx !== -1) { + const providerPart = directTarget.slice(0, slashIdx); + const modelPart = directTarget.slice(slashIdx + 1); + const provider = resolveProviderAlias(providerPart); + const canonicalModel = resolveProviderModelAlias(provider, modelPart); + return { provider, model: canonicalModel, extendedContext }; + } + } + // Resolve exact alias const resolved = resolveModelAliasTarget(parsed.model, aliases); if (resolved?.provider) { diff --git a/tests/unit/local-aliases-precedence.test.ts b/tests/unit/local-aliases-precedence.test.ts new file mode 100644 index 0000000000..54df2e1b8f --- /dev/null +++ b/tests/unit/local-aliases-precedence.test.ts @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getModelInfoCore } from "../../open-sse/services/model.ts"; + +// These tests cover the early-return path added in this PR (open-sse/services/model.ts): +// when a local alias map provides a slashful string target (e.g. "openai/gpt-4o"), +// the alias target is parsed directly as / and returned +// immediately, before shouldTreatAsExactModelId() / cross-proxy inference can +// misclassify the target. + +test("local alias with slashful string target returns provider/model directly", async () => { + const result = await getModelInfoCore("my-alias", { + "my-alias": "openai/gpt-4o", + }); + assert.deepEqual(result, { + provider: "openai", + model: "gpt-4o", + extendedContext: false, + }); +}); + +test("local alias early-return preserves extendedContext from [1m] suffix on input", async () => { + const result = await getModelInfoCore("my-alias[1m]", { + "my-alias": "openai/gpt-4o", + }); + assert.deepEqual(result, { + provider: "openai", + model: "gpt-4o", + extendedContext: true, + }); +}); + +test("local alias early-return preserves the slashful target's model part verbatim (no cross-proxy normalization)", async () => { + // The early-return is intentionally narrower than the object-target path: + // it only applies provider-scoped alias resolution, not the global + // cross-proxy normalization (CROSS_PROXY_MODEL_ALIASES). This mirrors the + // PR's intent — "user-provided 2nd arg wins, parsed directly" — and avoids + // double-resolving aliases the user already pinned to a canonical pair. + const result = await getModelInfoCore("sf-qwen", { + "sf-qwen": "siliconflow/qwen3-coder:480b", + }); + assert.deepEqual(result, { + provider: "siliconflow", + model: "qwen3-coder:480b", + extendedContext: false, + }); +}); + +test("local alias with non-slashful string target falls through to standard alias resolution", async () => { + // No "/" in the target → early-return guard doesn't fire; existing + // resolveModelAliasTarget path handles it. We assert that the legacy path + // still works and isn't shadowed by the new shortcut. + const result = await getModelInfoCore("legacy-alias", { + "legacy-alias": "gpt-oss:120b", + }); + // The legacy path resolves through cross-proxy / provider inference; + // we only assert that the early-return did NOT trip (model is normalized, + // not returned verbatim with provider=null,model="gpt-oss:120b"). + assert.notEqual(result.model, "gpt-oss:120b"); +}); + +test("local alias with object target (not string) falls through to standard resolution", async () => { + // The early-return guard checks `typeof aliases[parsed.model] === "string"`. + // Object targets must keep going through resolveModelAliasTarget. + const result = await getModelInfoCore("sf-qwen-obj", { + "sf-qwen-obj": { provider: "siliconflow", model: "qwen3-coder:480b" }, + }); + assert.deepEqual(result, { + provider: "siliconflow", + model: "Qwen/Qwen3-Coder-480B-A35B-Instruct", + extendedContext: false, + }); +});