Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/core/test/model-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,55 @@ describe("ModelAlias", () => {
const viaResolvedKey: ModelAlias.Aliases = { providers: { ds: "deepseek" }, models: { "deepseek/pro": "" } }
expect(() => ModelAlias.resolve(viaResolvedKey, "ds/pro")).toThrow("Invalid model reference: ds/pro")
})

test("25. Prototype member names are not aliases when not configured (#414)", () => {
const models: ModelAlias.Aliases = { models: { other: "openai/gpt-4o" } }
expect(() => ModelAlias.resolve(models, "toString")).toThrow(
"Invalid model reference after alias resolution: toString",
)
expect(() => ModelAlias.resolve(models, "constructor")).toThrow(
"Invalid model reference after alias resolution: constructor",
)
const providers: ModelAlias.Aliases = { providers: { other: "openai" } }
expect(ModelAlias.resolve(providers, "toString/model")).toBe("toString/model")
expect(ModelAlias.resolve(providers, "hasOwnProperty/model")).toBe("hasOwnProperty/model")
const variants: ModelAlias.Aliases = { variants: { hi: "high" } }
expect(ModelAlias.resolve(variants, "openai/gpt-4o#toString")).toBe("openai/gpt-4o#toString")
expect(ModelAlias.resolve(variants, "openai/gpt-4o#constructor")).toBe("openai/gpt-4o#constructor")
expect(ModelAlias.resolve(variants, "openai/gpt-4o#__proto__")).toBe("openai/gpt-4o#__proto__")
})

test("26. A configured alias named like a prototype member still resolves (#414)", () => {
expect(ModelAlias.resolve({ models: { toString: "openai/gpt-4o" } }, "toString")).toBe("openai/gpt-4o")
expect(ModelAlias.resolve({ providers: { toString: "openai" } }, "toString/model")).toBe("openai/model")
expect(ModelAlias.resolve({ variants: { constructor: "high" } }, "openai/gpt-4o#constructor")).toBe(
"openai/gpt-4o#high",
)
})

test("27. Whitespace-only or padded alias targets are rejected (#412)", () => {
const provider: ModelAlias.Aliases = { providers: { x: " " } }
expect(() => ModelAlias.resolve(provider, "x/model")).toThrow("Invalid model reference: x/model")
const paddedProvider: ModelAlias.Aliases = { providers: { x: " openai" } }
expect(() => ModelAlias.resolve(paddedProvider, "x/model")).toThrow("Invalid model reference: x/model")
const scoped: ModelAlias.Aliases = { models: { "openai/pro": " " } }
expect(() => ModelAlias.resolve(scoped, "openai/pro")).toThrow("Invalid model reference: openai/pro")
const viaResolved: ModelAlias.Aliases = { providers: { ds: "deepseek" }, models: { "deepseek/pro": " " } }
expect(() => ModelAlias.resolve(viaResolved, "ds/pro")).toThrow("Invalid model reference: ds/pro")
const global: ModelAlias.Aliases = { models: { bad: "openai/gpt-4o " } }
expect(() => ModelAlias.resolve(global, "bad")).toThrow("Invalid model reference: bad")
const paddedSegment: ModelAlias.Aliases = { models: { bad: "openai /gpt-4o" } }
expect(() => ModelAlias.resolve(paddedSegment, "bad")).toThrow("Invalid model reference: bad")
})

test("28. Whitespace-only or padded variant alias targets are rejected (#412)", () => {
const bare: ModelAlias.Aliases = { variants: { hi: " " } }
expect(() => ModelAlias.resolve(bare, "openai/gpt-4o#hi")).toThrow("Invalid model reference: openai/gpt-4o# ")
const tab: ModelAlias.Aliases = { variants: { hi: "\t" } }
expect(() => ModelAlias.resolve(tab, "openai/gpt-4o#hi")).toThrow("Invalid model reference: openai/gpt-4o#\t")
const scoped: ModelAlias.Aliases = { variants: { "deepseek/*#hi": " max" } }
expect(() => ModelAlias.resolve(scoped, "deepseek/other#hi")).toThrow(
"Invalid model reference: deepseek/other# max",
)
})
})
53 changes: 40 additions & 13 deletions packages/schema/src/model-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:

// Rule 1: Global model alias (no "/" in base)
if (!base.includes("/")) {
const aliasTarget = aliases.models?.[base]
const aliasTarget = ownAlias(aliases.models, base)
if (aliasTarget !== undefined) {
if (!aliasTarget) {
if (!aliasTarget || hasPaddedSegment(aliasTarget)) {
throw new Error(`Invalid model reference: ${current}`)
}
current = aliasTarget
Expand All @@ -105,9 +105,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:

// Check scoped model alias under current provider name first (e.g. "openai/gpt-4o")
const origScopedKey = `${provider}/${model}`
const origScopedAlias = aliases.models?.[origScopedKey]
const origScopedAlias = ownAlias(aliases.models, origScopedKey)
if (origScopedAlias !== undefined) {
if (!origScopedAlias) {
if (!origScopedAlias || hasPaddedSegment(origScopedAlias)) {
throw new Error(`Invalid model reference: ${current}`)
}
if (origScopedAlias.includes("/")) {
Expand All @@ -128,9 +128,14 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:
let modified = false

// Rule 2: Provider alias
const providerAlias = aliases.providers?.[provider]
const providerAlias = ownAlias(aliases.providers, provider)
if (providerAlias !== undefined && providerAlias !== provider) {
if (!providerAlias || providerAlias.includes("/") || providerAlias.includes("#")) {
if (
!providerAlias ||
providerAlias.includes("/") ||
providerAlias.includes("#") ||
providerAlias.trim() !== providerAlias
) {
throw new Error(`Invalid model reference: ${current}`)
}
provider = providerAlias
Expand All @@ -139,9 +144,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:

// Rule 3: Provider-scoped model alias under resolved provider
const scopedKey = `${provider}/${model}`
const scopedAlias = aliases.models?.[scopedKey]
const scopedAlias = ownAlias(aliases.models, scopedKey)
if (scopedAlias !== undefined) {
if (!scopedAlias) {
if (!scopedAlias || hasPaddedSegment(scopedAlias)) {
throw new Error(`Invalid model reference: ${current}`)
}
// Scoped alias target could be "newModel" or "newProvider/newModel"
Expand Down Expand Up @@ -222,27 +227,49 @@ function resolveVariant(
): string {
// 1. provider/model#variant
const key1 = `${provider}/${model}#${variant}`
if (key1 in variantAliases) return variantAliases[key1]
const value1 = ownAlias(variantAliases, key1)
if (value1 !== undefined) return checkedVariantTarget(value1, provider, model)

// 2. */model#variant
const key2 = `*/${model}#${variant}`
if (key2 in variantAliases) return variantAliases[key2]
const value2 = ownAlias(variantAliases, key2)
if (value2 !== undefined) return checkedVariantTarget(value2, provider, model)

// 3. provider/*#variant
const key3 = `${provider}/*#${variant}`
if (key3 in variantAliases) return variantAliases[key3]
const value3 = ownAlias(variantAliases, key3)
if (value3 !== undefined) return checkedVariantTarget(value3, provider, model)

// 4. */*#variant
const key4 = `*/*#${variant}`
if (key4 in variantAliases) return variantAliases[key4]
const value4 = ownAlias(variantAliases, key4)
if (value4 !== undefined) return checkedVariantTarget(value4, provider, model)

// 5. bare variant
if (variant in variantAliases) return variantAliases[variant]
const value5 = ownAlias(variantAliases, variant)
if (value5 !== undefined) return checkedVariantTarget(value5, provider, model)

// Real variant passes through
return variant
}

function ownAlias(map: Readonly<Record<string, string>> | undefined, key: string): string | undefined {
if (!map || !Object.hasOwn(map, key)) return undefined
const value = map[key]
return typeof value === "string" ? value : undefined
}

function hasPaddedSegment(target: string): boolean {
return target.split(/[/#]/).some((segment) => segment !== segment.trim())
}

function checkedVariantTarget(target: string, provider: string, model: string): string {
if (hasPaddedSegment(target)) {
throw new Error(`Invalid model reference: ${provider}/${model}#${target}`)
}
return target
}

function parseRefString(input: string): ResolvedRef {
const providerEnd = input.indexOf("/")
if (providerEnd <= 0) {
Expand Down