Skip to content
Open
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
5 changes: 0 additions & 5 deletions generated/skill-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -968,11 +968,6 @@
}
],
"validate": [
{
"pattern": "\\d+-\\d+[)'\"]",
"message": "Model slug uses hyphens — use dots not hyphens for version numbers (e.g., claude-sonnet-4.6)",
"severity": "error"
},
{
"pattern": "AI_GATEWAY_API_KEY",
"message": "Consider OIDC-based auth via vercel env pull for automatic token management — AI_GATEWAY_API_KEY works but requires manual rotation",
Expand Down
18 changes: 10 additions & 8 deletions skills/ai-gateway/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ metadata:
- '\bbun\s+(install|i|add)\s+[^\n]*@ai-sdk/gateway\b'
- '\byarn\s+add\s+[^\n]*@ai-sdk/gateway\b'
validate:
-
pattern: \d+-\d+[)'"]
message: 'Model slug uses hyphens — use dots not hyphens for version numbers (e.g., claude-sonnet-4.6)'
severity: error
# Removed: the prior rule `\d+-\d+[)'"]` was too broad — it fired on any
# hyphenated digit pair followed by a quote/paren, which produced false
# positives on date strings ("01-15-2024"), package versions, and on
# direct provider SDK calls like `anthropic('claude-sonnet-4-6')` where
# hyphens are CORRECT (Anthropic's canonical model IDs use hyphens; only
# the gateway slug form uses dots). See vercel/vercel-plugin#60.
-
pattern: AI_GATEWAY_API_KEY
message: 'Consider OIDC-based auth via vercel env pull for automatic token management — AI_GATEWAY_API_KEY works but requires manual rotation'
Expand Down Expand Up @@ -134,11 +136,11 @@ const result = await generateText({
## Model Slug Rules (Critical)

- Always use `provider/model` format (for example `openai/gpt-5.4`).
- Versioned slugs use dots for versions, not hyphens:
- Correct: `anthropic/claude-sonnet-4.6`
- Incorrect: `anthropic/claude-sonnet-4-6`
- Naming convention depends on which call surface you're using:
- **Vercel AI Gateway** (`gateway('provider/...')` or plain string): the gateway's catalog uses dots in version numbers — e.g. `anthropic/claude-opus-4.6`, `openai/gpt-5.4`. See [Vercel AI Gateway models](https://vercel.com/ai-gateway/models).
- **Direct provider SDK** (`anthropic('...')`, `openai('...')`): use the provider's native model IDs, which Anthropic publishes with hyphens — e.g. `claude-sonnet-4-6`, `claude-opus-4-7`. See [Anthropic models](https://docs.anthropic.com/en/docs/about-claude/models).
- Before hardcoding model IDs, call `gateway.getAvailableModels()` and pick from the returned IDs.
- Default text models: `openai/gpt-5.4` or `anthropic/claude-sonnet-4.6`.
- Default text models: `openai/gpt-5.4` or `anthropic/claude-opus-4.6`.
- Do not default to outdated choices like `openai/gpt-4o`.

```ts
Expand Down
2 changes: 1 addition & 1 deletion tests/posttooluse-chain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ describe("real-world chain and validate scenarios", () => {
`import { generateText } from 'ai';`,
``,
`const result = await generateText({`,
` model: anthropic('claude-sonnet-4.6'),`,
` model: anthropic('claude-sonnet-4-6'),`,
` prompt: 'Hello!',`,
`});`,
].join("\n");
Expand Down
54 changes: 31 additions & 23 deletions tests/posttooluse-validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,29 +187,37 @@ describe("posttooluse-validate.mjs", () => {
}
});

test("detects gateway from 'ai' with hyphenated model slug", async () => {
writeFileSync(testFile, [
`import { generateText, gateway } from 'ai';`,
``,
`const result = await generateText({`,
` model: gateway('anthropic/claude-sonnet-4-6'),`,
` prompt: 'Hello!',`,
`});`,
].join("\n"));
const { code, stdout } = await runHook({
tool_name: "Write",
tool_input: { file_path: testFile },
});
expect(code).toBe(0);
const result = JSON.parse(stdout);
expect(result.hookSpecificOutput).toBeDefined();
const ctx = result.hookSpecificOutput.additionalContext;
expect(ctx).toContain("VALIDATION");
expect(ctx).toContain("dots not hyphens");
const meta = extractPostValidation(result.hookSpecificOutput);
expect(meta).toBeDefined();
expect(meta.errorCount).toBeGreaterThan(0);
expect(meta.matchedSkills).toContain("ai-gateway");
test("hyphenated and dotted model slugs both pass — slug-form rule removed in #60", async () => {
// Issue #60: the prior `\d+-\d+[)'"]` rule false-positived on
// Anthropic's canonical hyphenated slugs and on date strings.
// Rule removed; both gateway-catalog (dots) and provider-canonical
// (hyphens) forms must pass without raising "dots not hyphens".
// ai-gateway must still be detected on the file (importPatterns
// includes 'ai'), and the missing-provider rule still fires for
// bare model strings.
for (const slug of ["anthropic/claude-sonnet-4-6", "anthropic/claude-sonnet-4.6"]) {
writeFileSync(testFile, [
`import { generateText, gateway } from 'ai';`,
``,
`const result = await generateText({`,
` model: gateway('${slug}'),`,
` prompt: 'Hello!',`,
`});`,
].join("\n"));
const { code, stdout } = await runHook({
tool_name: "Write",
tool_input: { file_path: testFile },
});
expect(code).toBe(0);
const result = JSON.parse(stdout);
const ctx = result.hookSpecificOutput?.additionalContext ?? "";
// The deleted rule's message must not appear for either form.
expect(ctx).not.toContain("dots not hyphens");
const meta = extractPostValidation(result.hookSpecificOutput);
if (meta) {
expect(meta.matchedSkills).toContain("ai-gateway");
}
}
});

test("no output for file that doesn't match any skill", async () => {
Expand Down
48 changes: 31 additions & 17 deletions tests/validate-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,15 +423,25 @@ describe("ai-sdk validation rules", () => {
// ---------------------------------------------------------------------------

describe("ai-gateway validation rules", () => {
test("flags hyphenated model slug (anthropic/claude-sonnet-4-6)", () => {
const data = loadRealRules();
const violations = runValidation(
test("does NOT flag hyphenated model slug — both gateway-form (dots) and provider-canonical (hyphens) are valid", () => {
// Issue #60: the prior rule `\d+-\d+[)'"]` flagged hyphenated model
// slugs as typos expecting dots. But Anthropic's canonical IDs are
// hyphenated (`claude-sonnet-4-6`), and the rule also false-positived
// on date strings, package versions, and direct provider SDK calls.
// Rule was removed; both forms must now pass without violation.
const data = loadRealRules();
const hyphenViolations = runValidation(
`gateway('anthropic/claude-sonnet-4-6')\n`,
["ai-gateway"],
data!.rulesMap,
);
// This pattern is a plain string, no escaping needed
expect(violations.some((v) => v.message.includes("dots not hyphens"))).toBe(true);
const dotViolations = runValidation(
`gateway('anthropic/claude-sonnet-4.6')\n`,
["ai-gateway"],
data!.rulesMap,
);
expect(hyphenViolations.some((v) => v.message.includes("dots not hyphens"))).toBe(false);
expect(dotViolations.some((v) => v.message.includes("dots not hyphens"))).toBe(false);
});

test("AI_GATEWAY_API_KEY is recommended severity (fallback auth)", () => {
Expand Down Expand Up @@ -1143,10 +1153,10 @@ describe("multi-skill overlap", () => {
const data = loadRealRules();
// Use patterns that actually work (no double-escape issues):
// ai-sdk: import from 'openai' (direct import pattern)
// ai-gateway: anthropic/claude-sonnet-4-6 (hyphenated slug)
// ai-gateway: gateway('opaque') — missing provider/ prefix (error rule)
const content = [
`import OpenAI from 'openai';`,
`const result = await generateText({ model: gateway('anthropic/claude-sonnet-4-6') });`,
`const result = await generateText({ model: gateway('opaque') });`,
].join("\n");
const violations = runValidation(content, ["ai-sdk", "ai-gateway"], data!.rulesMap);

Expand Down Expand Up @@ -1177,10 +1187,10 @@ describe("multi-skill overlap", () => {
test("overlapping rules don't suppress each other", () => {
const data = loadRealRules();
// ai-sdk flags: import from 'openai'
// ai-gateway flags: anthropic/claude-sonnet-4-6 (hyphenated slug)
// ai-gateway flags: gateway('opaque') — missing provider/ prefix
const content = [
`import OpenAI from 'openai';`,
`gateway('anthropic/claude-sonnet-4-6')`,
`gateway('opaque')`,
].join("\n");
const violations = runValidation(content, ["ai-sdk", "ai-gateway"], data!.rulesMap);

Expand All @@ -1195,12 +1205,12 @@ describe("multi-skill overlap", () => {
const content = [
`import OpenAI from 'openai';`, // line 1 - ai-sdk error
`const x = 1;`, // line 2 - clean
`gateway('anthropic/claude-sonnet-4-6')`, // line 3 - ai-gateway error
`gateway('opaque')`, // line 3 - ai-gateway error (missing provider/)
].join("\n");
const violations = runValidation(content, ["ai-sdk", "ai-gateway"], data!.rulesMap);

const aiSdkV = violations.find((v) => v.skill === "ai-sdk" && v.message.includes("@ai-sdk/openai"));
const aiGwV = violations.find((v) => v.skill === "ai-gateway" && v.message.includes("dots not hyphens"));
const aiGwV = violations.find((v) => v.skill === "ai-gateway" && v.message.includes("missing provider/ prefix"));
expect(aiSdkV).toBeDefined();
expect(aiSdkV!.line).toBe(1);
expect(aiGwV).toBeDefined();
Expand Down Expand Up @@ -1439,13 +1449,17 @@ describe("no false positives", () => {
expect(errors.length).toBe(0);
});

test("correctly versioned anthropic slug does not flag", () => {
test("anthropic slug — both dot and hyphen forms pass (rule removed in #60)", () => {
const data = loadRealRules();
const content = `gateway('anthropic/claude-sonnet-4.6')\n`;
const violations = runValidation(content, ["ai-gateway"], data!.rulesMap);
// The dot version should NOT be flagged (only hyphenated version is wrong)
const slugError = violations.filter((v) => v.message.includes("dots not hyphens"));
expect(slugError.length).toBe(0);
// Post-#60: the dot-vs-hyphen rule is gone. Both gateway-catalog form
// (dots, e.g. anthropic/claude-sonnet-4.6) and Anthropic-canonical form
// (hyphens, e.g. anthropic/claude-sonnet-4-6) must pass without
// raising a "dots not hyphens" violation.
for (const slug of ["anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-4-6"]) {
const violations = runValidation(`gateway('${slug}')\n`, ["ai-gateway"], data!.rulesMap);
const slugError = violations.filter((v) => v.message.includes("dots not hyphens"));
expect(slugError.length).toBe(0);
}
});
});

Expand Down