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
35 changes: 35 additions & 0 deletions packages/core/test/model-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,39 @@ describe("ModelAlias", () => {
"Invalid model reference: deepseek/other# max",
)
})
test("30. Padded input reference segments are rejected (#436)", () => {
const aliases: ModelAlias.Aliases = { models: { zzz: "a/b" } }
expect(() => ModelAlias.resolve(aliases, "openai /gpt-4o")).toThrow("Invalid model reference: openai /gpt-4o")
expect(() => ModelAlias.resolve(aliases, "openai/ gpt-4o")).toThrow("Invalid model reference: openai/ gpt-4o")
expect(() => ModelAlias.resolve(aliases, "openai/gpt-4o# fast")).toThrow(
"Invalid model reference: openai/gpt-4o# fast",
)
expect(() => ModelAlias.resolve(undefined, "openai /gpt-4o")).toThrow("Invalid model reference: openai /gpt-4o")
expect(() => ModelAlias.resolve(undefined, "openai/ gpt-4o")).toThrow("Invalid model reference: openai/ gpt-4o")
expect(() => ModelAlias.resolve(undefined, "openai/gpt-4o# fast")).toThrow(
"Invalid model reference: openai/gpt-4o# fast",
)
expect(ModelAlias.resolve(aliases, " openai/gpt-4o ")).toBe("openai/gpt-4o")
})

test("31. References with empty segments are rejected (#437)", () => {
const aliases: ModelAlias.Aliases = { models: { zzz: "a/b" } }
expect(() => ModelAlias.resolve(aliases, "openai//gpt")).toThrow("Invalid model reference: openai//gpt")
expect(() => ModelAlias.resolve(aliases, "openai/gpt/")).toThrow("Invalid model reference: openai/gpt/")
expect(() => ModelAlias.resolve(undefined, "openai//gpt")).toThrow("Invalid model reference: openai//gpt")
expect(() => ModelAlias.resolve(undefined, "openai/gpt/")).toThrow("Invalid model reference: openai/gpt/")
const doubledTarget: ModelAlias.Aliases = { models: { x: "openai//gpt" } }
expect(() => ModelAlias.resolve(doubledTarget, "x")).toThrow(
"Invalid model reference after alias resolution: openai//gpt",
)
const trailingTarget: ModelAlias.Aliases = { models: { x: "a//" } }
expect(() => ModelAlias.resolve(trailingTarget, "x")).toThrow("Invalid model reference after alias resolution: a//")
expect(ModelAlias.resolve(aliases, "openrouter/anthropic/claude-sonnet-4")).toBe(
"openrouter/anthropic/claude-sonnet-4",
)
expect(ModelAlias.resolve(undefined, "openrouter/anthropic/claude-sonnet-4")).toBe(
"openrouter/anthropic/claude-sonnet-4",
)
})

})
16 changes: 16 additions & 0 deletions packages/schema/src/model-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:
if (!trimmed) {
throw new Error("Empty model reference cannot be resolved")
}
if (hasPaddedSegment(trimmed) || hasEmptySegment(trimmed)) {
throw new Error(`Invalid model reference: ${trimmed}`)
}

if (!aliases || (!aliases.providers && !aliases.models && !aliases.variants)) {
return parseRefString(trimmed)
Expand Down Expand Up @@ -191,6 +194,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:
if (slashIdx <= 0 || slashIdx === settledBase.length - 1) {
throw new Error(`Invalid model reference after alias resolution: ${settledBase}`)
}
if (hasPaddedSegment(settledBase) || hasEmptySegment(settledBase)) {
throw new Error(`Invalid model reference after alias resolution: ${settledBase}`)
}

const finalProvider = settledBase.slice(0, slashIdx)
const finalModel = settledBase.slice(slashIdx + 1)
Expand All @@ -204,6 +210,9 @@ export function resolveRef(aliases: Aliases | undefined, input: string, exists?:
throw new Error(`Invalid model reference: ${finalProvider}/${finalModel}#${finalVariant}`)
}
}
if (finalVariant !== undefined && (hasPaddedSegment(finalVariant) || hasEmptySegment(finalVariant))) {
throw new Error(`Invalid model reference: ${finalProvider}/${finalModel}#${finalVariant}`)
}

const canonicalRef = finalVariant ? `${finalProvider}/${finalModel}#${finalVariant}` : `${finalProvider}/${finalModel}`

Expand Down Expand Up @@ -267,6 +276,10 @@ function hasPaddedSegment(target: string): boolean {
return target.split(/[/#]/).some((segment) => segment !== segment.trim())
}

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

function checkedVariantTarget(target: string, provider: string, model: string): string {
if (hasPaddedSegment(target)) {
throw new Error(`Invalid model reference: ${provider}/${model}#${target}`)
Expand All @@ -275,6 +288,9 @@ function checkedVariantTarget(target: string, provider: string, model: string):
}

function parseRefString(input: string): ResolvedRef {
if (hasPaddedSegment(input) || hasEmptySegment(input)) {
throw new Error(`Invalid model reference: ${input}`)
}
const providerEnd = input.indexOf("/")
if (providerEnd <= 0) {
throw new Error(`Invalid model reference: ${input}`)
Expand Down