-
Notifications
You must be signed in to change notification settings - Fork 112
feat: add vercel-ai-sdk.mdc rule to prevent v6 hallucinations #2727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,230 @@ | ||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
| description: Correct AI SDK patterns for this project — prevents hallucination of outdated APIs | ||||||||||||||||||||||||||||||||||||||
| globs: packages/giselle/src/generations/**/*.ts | ||||||||||||||||||||||||||||||||||||||
| alwaysApply: false | ||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # AI SDK — Correct Patterns for This Project | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| > **How this rule differs from `update-ai-sdk.mdc`:** That rule covers *upgrading SDK versions* (running `pnpm outdated`, updating the catalog). This rule covers *writing correct code* against the current pinned SDK version. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| LLMs frequently hallucinate outdated AI SDK APIs. Follow these rules strictly. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 1. `toolContext` → `experimental_context` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **NEVER** use `toolContext`. It was removed. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT | ||||||||||||||||||||||||||||||||||||||
| import { tool } from "ai"; | ||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const myTool = tool({ | ||||||||||||||||||||||||||||||||||||||
| description: "Do something with user context", | ||||||||||||||||||||||||||||||||||||||
| inputSchema: z.object({ | ||||||||||||||||||||||||||||||||||||||
| query: z.string().describe("The search query"), | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| execute: async (input, { experimental_context }) => { | ||||||||||||||||||||||||||||||||||||||
| const ctx = experimental_context as { userId: string }; | ||||||||||||||||||||||||||||||||||||||
| // use ctx.userId | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Pass context at the call site: | ||||||||||||||||||||||||||||||||||||||
| const result = streamText({ | ||||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||||
| tools: { myTool }, | ||||||||||||||||||||||||||||||||||||||
| experimental_context: { userId: "123" }, | ||||||||||||||||||||||||||||||||||||||
| // ... | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — removed API, will fail silently | ||||||||||||||||||||||||||||||||||||||
| execute: async (args, { toolContext }) => { ... } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#context-experimental | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 2. `parameters` → `inputSchema` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The `tool()` function property for defining input is `inputSchema`, not `parameters`. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT | ||||||||||||||||||||||||||||||||||||||
| tool({ | ||||||||||||||||||||||||||||||||||||||
| description: "Get weather", | ||||||||||||||||||||||||||||||||||||||
| inputSchema: z.object({ | ||||||||||||||||||||||||||||||||||||||
| location: z.string().describe("City name"), | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| execute: async ({ location }) => { ... }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — old property name | ||||||||||||||||||||||||||||||||||||||
| tool({ | ||||||||||||||||||||||||||||||||||||||
| parameters: z.object({ ... }), // Does not exist | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#tool-calling | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 3. `strict: true` is a Tool Option (not Zod) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| `strict` is a **top-level property on the tool**, not a method on the Zod schema. Always add `.describe()` to every field. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT | ||||||||||||||||||||||||||||||||||||||
| tool({ | ||||||||||||||||||||||||||||||||||||||
| description: "Get weather", | ||||||||||||||||||||||||||||||||||||||
| inputSchema: z.object({ | ||||||||||||||||||||||||||||||||||||||
| location: z.string().describe("City name, e.g. London"), | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| strict: true, // <-- top-level tool option | ||||||||||||||||||||||||||||||||||||||
| execute: async ({ location }) => { ... }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — .strict() is a Zod method, not the SDK's strict mode | ||||||||||||||||||||||||||||||||||||||
| inputSchema: z.object({ ... }).strict(), | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#strict-mode | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 4. `toDataStreamResponse()` → `toUIMessageStream()` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| This project uses `toUIMessageStream()` — not `toDataStreamResponse()` or `toUIMessageStreamResponse()`. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT — what this project uses | ||||||||||||||||||||||||||||||||||||||
| const uiMessageStream = streamTextResult.toUIMessageStream({ | ||||||||||||||||||||||||||||||||||||||
| onFinish: async ({ messages }) => { | ||||||||||||||||||||||||||||||||||||||
| // handle completion | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — outdated helpers | ||||||||||||||||||||||||||||||||||||||
| result.toDataStreamResponse(); | ||||||||||||||||||||||||||||||||||||||
| result.toUIMessageStreamResponse(); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+119
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
📝 Suggested wording fix // ❌ WRONG — outdated helpers
result.toDataStreamResponse();
-result.toUIMessageStreamResponse();
+// ❌ AVOID in this project — wraps the stream into a Response directly;
+// use toUIMessageStream() and compose the response via createUIMessageStreamResponse()
+// result.toUIMessageStreamResponse();🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 5. `maxSteps` → `stopWhen` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Multi-step tool calls use `stopWhen`, not `maxSteps`. Import `stepCountIs` from `"ai"` for simple step limits, or use a custom function. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT — simple step limit | ||||||||||||||||||||||||||||||||||||||
| import { streamText, stepCountIs } from "ai"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const result = streamText({ | ||||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||||
| stopWhen: stepCountIs(5), | ||||||||||||||||||||||||||||||||||||||
| tools: { ... }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT — custom stop condition (used in this project) | ||||||||||||||||||||||||||||||||||||||
| stopWhen: ({ steps }) => { | ||||||||||||||||||||||||||||||||||||||
| const lastStep = steps[steps.length - 1]; | ||||||||||||||||||||||||||||||||||||||
| return lastStep.finishReason !== "tool-calls"; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — deprecated option | ||||||||||||||||||||||||||||||||||||||
| streamText({ maxSteps: 5, ... }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls-using-stopwhen | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 6. Messages: `UIMessage` + `ModelMessage` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Client messages are `UIMessage` type. Model calls expect `ModelMessage`. Use `convertToModelMessages()` to convert between them. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ✅ CORRECT | ||||||||||||||||||||||||||||||||||||||
| import { streamText, type UIMessage, type ModelMessage } from "ai"; | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // ❌ WRONG — using untyped message arrays | ||||||||||||||||||||||||||||||||||||||
| const { messages } = await req.json(); | ||||||||||||||||||||||||||||||||||||||
| streamText({ messages, ... }); // Missing type safety | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+153
to
+166
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rule 6's ✅ CORRECT example never demonstrates The prose on line 155 says to use Compounding this, the canonical pattern (lines 211–213) passes 📝 Proposed example additions // ✅ CORRECT
-import { streamText, type UIMessage, type ModelMessage } from "ai";
+import { streamText, convertToModelMessages, type UIMessage } from "ai";
+
+// In your route handler:
+const { messages }: { messages: UIMessage[] } = await req.json();
+
+const result = streamText({
+ model,
+ messages: convertToModelMessages(messages), // UIMessage[] → ModelMessage[]
+ tools: { ... },
+});And in the canonical pattern (line 213): - messages,
+ messages: convertToModelMessages(messages),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/getting-started/nextjs-app-router#create-a-route-handler | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## 7. `generateObject` — Heads-Up for Future Migration | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| > **Note:** This project currently uses `generateObject` which is valid for our pinned SDK version. However, upstream AI SDK v6 has deprecated `generateObject` and `streamObject` in favor of `generateText` / `streamText` with `Output.object({ schema })`. Keep this in mind for the next SDK upgrade. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| // Current (v5) — valid for this project | ||||||||||||||||||||||||||||||||||||||
| import { generateObject } from "ai"; | ||||||||||||||||||||||||||||||||||||||
| const { object } = await generateObject({ model, schema, prompt }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Future (v6) — for when the project upgrades | ||||||||||||||||||||||||||||||||||||||
| import { generateText, Output } from "ai"; | ||||||||||||||||||||||||||||||||||||||
| const { output } = await generateText({ | ||||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||||
| output: Output.object({ schema }), | ||||||||||||||||||||||||||||||||||||||
| prompt, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Source:** https://sdk.vercel.ai/docs/ai-sdk-core/generating-structured-data#generateobject-and-streamobject-legacy | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Reference: Canonical Streaming Pattern | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| A correct streaming setup following this project's conventions: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| streamText, | ||||||||||||||||||||||||||||||||||||||
| stepCountIs, | ||||||||||||||||||||||||||||||||||||||
| smoothStream, | ||||||||||||||||||||||||||||||||||||||
| type UIMessage, | ||||||||||||||||||||||||||||||||||||||
| } from "ai"; | ||||||||||||||||||||||||||||||||||||||
| import { createGateway } from "@ai-sdk/gateway"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const gateway = createGateway({ | ||||||||||||||||||||||||||||||||||||||
| headers: aiGatewayHeaders, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const streamTextResult = streamText({ | ||||||||||||||||||||||||||||||||||||||
| model: gateway("openai/gpt-4o"), | ||||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||||
| tools: toolSet, | ||||||||||||||||||||||||||||||||||||||
| stopWhen: stepCountIs(5), | ||||||||||||||||||||||||||||||||||||||
| experimental_transform: smoothStream({ | ||||||||||||||||||||||||||||||||||||||
| delayInMs: 1000, | ||||||||||||||||||||||||||||||||||||||
| chunking: "line", | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| onError: ({ error }) => { | ||||||||||||||||||||||||||||||||||||||
| // handle error | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const uiMessageStream = streamTextResult.toUIMessageStream({ | ||||||||||||||||||||||||||||||||||||||
| onFinish: async ({ messages: generateMessages }) => { | ||||||||||||||||||||||||||||||||||||||
| // handle completion | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+194
to
+230
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Canonical "Route Handler" pattern is incomplete: no HTTP response is returned. The PR describes this as a "canonical Route Handler reference template for copy-paste," but the snippet ends after creating It should close with something like: const uiMessageStream = streamTextResult.toUIMessageStream({
onFinish: async ({ messages: generateMessages }) => {
// handle completion
},
});
+
+return new Response(uiMessageStream, {
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
+});
+// — or, using the SDK helper —
+// import { createUIMessageStreamResponse } from "ai";
+// return createUIMessageStreamResponse({ stream: uiMessageStream });🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. V6 rule vs repo ai@5
🐞 Bug✓ CorrectnessAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools