From 26cda3578957c9ff3cacb21ee6c8a08dc49118a7 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Sun, 25 Oct 2026 07:05:42 -0500 Subject: [PATCH] fix(schema): reject padded and empty segments in model references --- packages/core/test/model-alias.test.ts | 35 ++++++++++++++++++++++++++ packages/schema/src/model-alias.ts | 16 ++++++++++++ 2 files changed, 51 insertions(+) diff --git a/packages/core/test/model-alias.test.ts b/packages/core/test/model-alias.test.ts index e78f6ebd23da..ec550a71ae1f 100644 --- a/packages/core/test/model-alias.test.ts +++ b/packages/core/test/model-alias.test.ts @@ -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", + ) + }) + }) diff --git a/packages/schema/src/model-alias.ts b/packages/schema/src/model-alias.ts index 5bc52bb9ee9c..ecb71176e588 100644 --- a/packages/schema/src/model-alias.ts +++ b/packages/schema/src/model-alias.ts @@ -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) @@ -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) @@ -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}` @@ -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}`) @@ -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}`)