diff --git a/src/constants.ts b/src/constants.ts index b9f151e3..ca74925f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -655,7 +655,9 @@ export function isValidModelId(value: string): boolean { return ( modelId.length > 0 && modelId.length <= 120 && - /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/u.test(modelId) && + // Leading @ for Cloudflare Workers AI ids (@cf/...); interior @ for + // Vertex AI @-versioned ids (e.g. claude-sonnet-4-5@20250929). + /^[@A-Za-z0-9][A-Za-z0-9._:/@+-]*$/u.test(modelId) && !modelId.includes("://") ); } diff --git a/test/constants.test.ts b/test/constants.test.ts index 52d85bff..40db95d5 100644 --- a/test/constants.test.ts +++ b/test/constants.test.ts @@ -38,6 +38,12 @@ describe("isValidModelId", () => { expect(isValidModelId("nvidia/nemotron-3-super-120b-a12b")).toBe(true); }); + test("accepts Cloudflare Workers AI model ids with leading '@'", () => { + expect(isValidModelId("@cf/meta/llama-3-8b-instruct")).toBe(true); + expect(isValidModelId("@cf/moonshotai/kimi-k2.7-code")).toBe(true); + expect(isValidModelId("@cf/qwen/qwen1.5-14b-chat-awq")).toBe(true); + }); + test("rejects empty, whitespace-only, and over-long ids", () => { expect(isValidModelId("")).toBe(false); expect(isValidModelId(" ")).toBe(false); @@ -53,10 +59,11 @@ describe("isValidModelId", () => { expect(isValidModelId("claude-haiku-4-5@20251001")).toBe(true); }); - test("rejects ids starting with a non-alphanumeric character", () => { + test("rejects ids starting with a disallowed non-alphanumeric character", () => { expect(isValidModelId("-leading-dash")).toBe(false); expect(isValidModelId("/leading-slash")).toBe(false); - expect(isValidModelId("@leading-at")).toBe(false); + // A leading "@" is intentionally allowed for Cloudflare Workers AI ids + // (covered above), so it is not rejected here. }); test("normalizeModelId trims surrounding whitespace", () => {