diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml deleted file mode 100644 index 6a27f1d21c61..000000000000 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Bug report -description: Report a bug with the AI SDK. -labels: ['bug'] -body: - - type: markdown - attributes: - value: | - This template is to report bugs. If you need help with your own project, feel free to [start a new thread in our discussions](https://github.com/vercel/ai/discussions). - - type: textarea - attributes: - label: Description - description: A detailed description of the issue that you are encountering with the AI SDK, and how other people can reproduce it. This includes helpful information such as the API you are using, the framework and AI provider. - placeholder: | - Reproduction steps... - validations: - required: true - - type: textarea - attributes: - label: Code example - description: Provide an example code snippet that has the problem. - placeholder: | - import { openai } from '@ai-sdk/openai'; - import { streamText } from 'ai'; - ... - - type: input - id: provider - attributes: - label: AI provider - description: The AI provider (e.g. `@ai-sdk/openai`) that you are using, and its version (e.g. `1.0.0`). - placeholder: | - @ai-sdk/openai v1.0.0 - - type: textarea - attributes: - label: Additional context - description: | - Any extra information that might help us investigate. diff --git a/.github/ISSUE_TEMPLATE/1.support_request.yml b/.github/ISSUE_TEMPLATE/1.support_request.yml new file mode 100644 index 000000000000..0ea8b98fd42d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.support_request.yml @@ -0,0 +1,27 @@ +name: Support Request +description: Report a bug, feature request or other issue with the AI SDK. +labels: ['support'] +body: + - type: markdown + attributes: + value: | + This template is ask for help regarding an issue that could be a bug or a feature request. + - type: textarea + attributes: + label: Description + description: A detailed description. Please include relevant information such as reproduction steps, code examples, and any other information that might help us understand the issue. + placeholder: | + Reproduction steps, code examples, background, etc... + validations: + required: true + - type: textarea + attributes: + label: AI SDK Version + description: Which version of the AI SDK are you using? + placeholder: | + Examples: + - ai: 4.1.2 + - @ai-sdk/react: 2.1.0 + - @ai-sdk/openai: 0.5.2 + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/2.feature_request.yml deleted file mode 100644 index 2c7355ea3698..000000000000 --- a/.github/ISSUE_TEMPLATE/2.feature_request.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Feature Request -description: Propose a new feature for the AI SDK. -labels: ['enhancement'] -body: - - type: markdown - attributes: - value: | - This template is to propose new features for the AI SDK. If you need help with your own project, feel free to [start a new thread in our discussions](https://github.com/vercel/ai/discussions). - - type: textarea - attributes: - label: Feature Description - description: A detailed description of the feature you are proposing for the SDK. - placeholder: | - Feature description... - validations: - required: true - - type: textarea - attributes: - label: Use Cases - description: Provide use cases where this feature would be beneficial. - placeholder: | - Use case... - - type: textarea - attributes: - label: Additional context - description: | - Any extra information that might help us understand your feature request. - placeholder: | - Additional context... diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000000..8e26c04c99a3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,51 @@ + + +## Background + + + +## Summary + + + +## Verification + + + +## Tasks + + + +- [ ] Tests have been added / updated (for bug fixes / features) +- [ ] Documentation has been added / updated (for bug fixes / features) +- [ ] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) +- [ ] Formatting issues have been fixed (run `pnpm prettier-fix` in the project root) + +## Future Work + + + +## Related Issues + + diff --git a/.github/workflows/actions/verify-changesets/index.js b/.github/workflows/actions/verify-changesets/index.js new file mode 100644 index 000000000000..fa21a3fb171b --- /dev/null +++ b/.github/workflows/actions/verify-changesets/index.js @@ -0,0 +1,137 @@ +import fs from 'node:fs/promises'; + +const BYPASS_LABELS = ['minor', 'major']; + +// check if current file is the entry point +if (import.meta.url.endsWith(process.argv[1])) { + // https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request + const pullRequestEvent = JSON.parse( + await fs.readFile(process.env.GITHUB_EVENT_PATH, 'utf-8'), + ); + + try { + const message = await verifyChangesets( + pullRequestEvent, + process.env, + fs.readFile, + ); + await fs.writeFile( + process.env.GITHUB_STEP_SUMMARY, + `## Changeset verification passed ✅\n\n${message || ''}`, + ); + } catch (error) { + // write error to summary + console.error(error.message); + await fs.writeFile( + process.env.GITHUB_STEP_SUMMARY, + `## Changeset verification failed ❌ + +${error.message}`, + ); + + if (error.path) { + await fs.appendFile( + process.env.GITHUB_STEP_SUMMARY, + `\n\nFile: \`${error.path}\``, + ); + } + + if (error.content) { + await fs.appendFile( + process.env.GITHUB_STEP_SUMMARY, + `\n\n\`\`\`yaml\n${error.content}\n\`\`\``, + ); + } + + process.exit(1); + } +} + +export async function verifyChangesets( + event, + env = process.env, + readFile = fs.readFile, +) { + // Skip check if pull request has "minor-release" label + const byPassLabel = event.pull_request.labels.find(label => + BYPASS_LABELS.includes(label.name), + ); + if (byPassLabel) { + return `Skipping changeset verification - "${byPassLabel.name}" label found`; + } + + // Iterate through all changed .changeset/*.md files + for (const path of env.CHANGED_FILES.trim().split(' ')) { + // ignore README.md file + if (path === '.changeset/README.md') continue; + + // Check if the file is a .changeset file + if (!/^\.changeset\/[a-z-]+\.md/.test(path)) { + throw Object.assign(new Error(`Invalid file - not a .changeset file`), { + path, + }); + } + + // find frontmatter + const content = await readFile(`../../../../${path}`, 'utf-8'); + const result = content.match(/---\n([\s\S]+?)\n---/); + if (!result) { + throw Object.assign( + new Error(`Invalid .changeset file - no frontmatter found`), + { + path, + content, + }, + ); + } + + const [frontmatter] = result; + + // Find version bump by package. `frontmatter` looks like this: + // + // ```yaml + // 'ai': patch + // '@ai-sdk/provider': patch + // ``` + const lines = frontmatter.split('\n').slice(1, -1); + const versionBumps = {}; + for (const line of lines) { + const [packageName, versionBump] = line.split(':').map(s => s.trim()); + if (!packageName || !versionBump) { + throw Object.assign( + new Error(`Invalid .changeset file - invalid frontmatter`, { + path, + content, + }), + ); + } + + // Check if packageName is already set + if (versionBumps[packageName]) { + throw Object.assign( + new Error( + `Invalid .changeset file - duplicate package name "${packageName}"`, + ), + { path, content }, + ); + } + + versionBumps[packageName] = versionBump; + } + + // check if any of the version bumps are not "patch" + const invalidVersionBumps = Object.entries(versionBumps).filter( + ([, versionBump]) => versionBump !== 'patch', + ); + + if (invalidVersionBumps.length > 0) { + throw Object.assign( + new Error( + `Invalid .changeset file - invalid version bump (only "patch" is allowed, see https://ai-sdk.dev/docs/migration-guides/versioning). To bypass, add one of the following labels: ${BYPASS_LABELS.join(', ')}`, + ), + + { path, content }, + ); + } + } +} diff --git a/.github/workflows/actions/verify-changesets/package.json b/.github/workflows/actions/verify-changesets/package.json new file mode 100644 index 000000000000..bff806df20e3 --- /dev/null +++ b/.github/workflows/actions/verify-changesets/package.json @@ -0,0 +1,8 @@ +{ + "name": "verify-changesets-action", + "private": true, + "type": "module", + "scripts": { + "test": "node --test test.js" + } +} diff --git a/.github/workflows/actions/verify-changesets/test.js b/.github/workflows/actions/verify-changesets/test.js new file mode 100644 index 000000000000..0c4e024f38ef --- /dev/null +++ b/.github/workflows/actions/verify-changesets/test.js @@ -0,0 +1,193 @@ +import assert from 'node:assert'; +import { mock, test } from 'node:test'; + +import { verifyChangesets } from './index.js'; + +test('happy path', async () => { + const event = { + pull_request: { + labels: [], + }, + }; + const env = { + CHANGED_FILES: '.changeset/some-happy-path.md', + }; + + const readFile = mock.fn(async path => { + return `---\nai: patch\n@ai-sdk/provider: patch\n---\n## Test changeset`; + }); + + await verifyChangesets(event, env, readFile); + + assert.strictEqual(readFile.mock.callCount(), 1); + assert.deepStrictEqual(readFile.mock.calls[0].arguments, [ + '../../../../.changeset/some-happy-path.md', + 'utf-8', + ]); +}); + +test('ignores .changeset/README.md', async () => { + const event = { + pull_request: { + labels: [], + }, + }; + const env = { + CHANGED_FILES: '.changeset/README.md', + }; + + const readFile = mock.fn(() => {}); + + await verifyChangesets(event, env, readFile); + + assert.strictEqual(readFile.mock.callCount(), 0); +}); + +test('invalid file - not a .changeset file', async () => { + const event = { + pull_request: { + labels: [], + }, + }; + const env = { + CHANGED_FILES: '.changeset/not-a-changeset-file.txt', + }; + + const readFile = mock.fn(() => {}); + + await assert.rejects( + () => verifyChangesets(event, env, readFile), + Object.assign(new Error('Invalid file - not a .changeset file'), { + path: '.changeset/not-a-changeset-file.txt', + }), + ); + + assert.strictEqual(readFile.mock.callCount(), 0); +}); + +test('invalid .changeset file - no frontmatter', async () => { + const event = { + pull_request: { + labels: [], + }, + }; + const env = { + CHANGED_FILES: '.changeset/invalid-changeset-file.md', + }; + + const readFile = mock.fn(async path => { + return 'frontmatter missing'; + }); + await assert.rejects( + () => verifyChangesets(event, env, readFile), + Object.assign(new Error('Invalid .changeset file - no frontmatter found'), { + path: '.changeset/invalid-changeset-file.md', + content: 'frontmatter missing', + }), + ); + assert.strictEqual(readFile.mock.callCount(), 1); + assert.deepStrictEqual(readFile.mock.calls[0].arguments, [ + '../../../../.changeset/invalid-changeset-file.md', + 'utf-8', + ]); +}); + +test('minor update', async () => { + const event = { + pull_request: { + labels: [], + }, + }; + const env = { + CHANGED_FILES: '.changeset/patch-update.md .changeset/minor-update.md', + }; + + const readFile = mock.fn(async path => { + if (path.endsWith('patch-update.md')) { + return `---\nai: patch\n---\n## Test changeset`; + } + + return `---\n@ai-sdk/provider: minor\n---\n## Test changeset`; + }); + + await assert.rejects( + () => verifyChangesets(event, env, readFile), + Object.assign( + new Error( + `Invalid .changeset file - invalid version bump (only "patch" is allowed, see https://ai-sdk.dev/docs/migration-guides/versioning). To bypass, add one of the following labels: minor, major`, + ), + { + path: '.changeset/minor-update.md', + content: '---\n@ai-sdk/provider: minor\n---\n## Test changeset', + }, + ), + ); + + assert.strictEqual(readFile.mock.callCount(), 2); + assert.deepStrictEqual(readFile.mock.calls[0].arguments, [ + '../../../../.changeset/patch-update.md', + 'utf-8', + ]); + assert.deepStrictEqual(readFile.mock.calls[1].arguments, [ + '../../../../.changeset/minor-update.md', + 'utf-8', + ]); +}); + +test('minor update - with "minor" label', async () => { + const event = { + pull_request: { + labels: [ + { + name: 'minor', + }, + ], + }, + }; + const env = { + CHANGED_FILES: '.changeset/patch-update.md .changeset/minor-update.md', + }; + + const readFile = mock.fn(async path => { + if (path.endsWith('patch-update.md')) { + return `---\nai: patch\n---\n## Test changeset`; + } + + return `---\n@ai-sdk/provider: minor\n---\n## Test changeset`; + }); + + const message = await verifyChangesets(event, env, readFile); + assert.strictEqual( + message, + 'Skipping changeset verification - "minor" label found', + ); +}); + +test('major update - with "major" label', async () => { + const event = { + pull_request: { + labels: [ + { + name: 'major', + }, + ], + }, + }; + const env = { + CHANGED_FILES: '.changeset/patch-update.md .changeset/major-update.md', + }; + + const readFile = mock.fn(async path => { + if (path.endsWith('patch-update.md')) { + return `---\nai: patch\n---\n## Test changeset`; + } + + return `---\n@ai-sdk/provider: major\n---\n## Test changeset`; + }); + + const message = await verifyChangesets(event, env, readFile); + assert.strictEqual( + message, + 'Skipping changeset verification - "major" label found', + ); +}); diff --git a/.github/workflows/assign-team-pull-request.yml b/.github/workflows/assign-team-pull-request.yml new file mode 100644 index 000000000000..d13f6c880c20 --- /dev/null +++ b/.github/workflows/assign-team-pull-request.yml @@ -0,0 +1,21 @@ +name: Assign Team Pull Requests to Author + +on: + pull_request: + types: [opened] + +permissions: + pull-requests: write + +jobs: + assign: + runs-on: ubuntu-latest + steps: + # Only assign pull requests by team members, ignore pull requests from forks + - if: github.event.pull_request.head.repo.full_name == github.repository + name: Assign pull request to author + run: gh pr edit $PULL_REQUEST_URL --add-assignee $AUTHOR_LOGIN + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }} + AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7de754472594..e96c999fcd35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, v5] pull_request: - branches: [main] + branches: [main, v5] jobs: test: @@ -23,7 +23,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 42a185321759..2cb6400f8c37 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -2,9 +2,9 @@ name: Quality on: push: - branches: [main] + branches: [main, v5] pull_request: - branches: [main] + branches: [main, v5] jobs: prettier: @@ -17,7 +17,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Use Node.js 22 uses: actions/setup-node@v4 @@ -41,7 +41,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Use Node.js 22 uses: actions/setup-node@v4 @@ -65,7 +65,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Use Node.js 22 uses: actions/setup-node@v4 diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index e58a9e0bfd8f..97e198e40651 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -45,7 +45,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Setup Node.js 22 uses: actions/setup-node@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12eec2f442e2..502263e20ebd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - v5 paths: - '.changeset/**' - '.github/workflows/release.yml' @@ -25,7 +26,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.3 + version: 10.11.0 - name: Setup Node.js 22 uses: actions/setup-node@v4 diff --git a/.github/workflows/verify-changesets.yml b/.github/workflows/verify-changesets.yml new file mode 100644 index 000000000000..4feca6aef7bb --- /dev/null +++ b/.github/workflows/verify-changesets.yml @@ -0,0 +1,38 @@ +# vercel/ai uses https://github.com/changesets/changesets for versioning and changelogs, +# but is not following semantic versioning. Instead, it uses `patch` for both fixes +# and features. It uses `minor` for "marketing releases", accompanied by a blog post and migration guide. +# This workflow verifies that all `.changeset/*.md` files use `patch` unless a `minor-release` label is present. +name: Verify Changesets + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: + - main + paths: + - '.changeset/*.md' + +jobs: + verify-changesets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: get all changed files from .changeset/*.md + id: changeset-files + run: | + echo "changed-files=$(git diff --diff-filter=dr --name-only $BASE_SHA -- '.changeset/*.md' | tr '\n' ' ')" >> $GITHUB_OUTPUT + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + - name: Verify changesets + if: steps.changeset-files.outputs.changed-files != '' + working-directory: .github/workflows/actions/verify-changesets + run: | + node index.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGED_FILES: ${{ steps.changeset-files.outputs.changed-files }} diff --git a/.npmrc b/.npmrc index 2a53e07c0db1..13562b7874fa 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,4 @@ auto-install-peers = true -link-workspace-packages = true \ No newline at end of file +link-workspace-packages = true +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f54b62a9ff..1a47ed61b414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ You can find the changelogs for the individual packages in their respective `CHA - [@ai-sdk/openai-compatible](./packages/openai-compatible/CHANGELOG.md) - [@ai-sdk/perplexity](./packages/perplexity/CHANGELOG.md) - [@ai-sdk/togetherai](./packages/togetherai/CHANGELOG.md) +- [@ai-sdk/vercel](./packages/vercel/CHANGELOG.md) - [@ai-sdk/xai](./packages/xai/CHANGELOG.md) ### UI integrations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9644053c4a2..1f0dc9fcc63f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ To set up the repository on your local machine, follow these steps: 1. **Fork the Repository**: Make a copy of the repository to your GitHub account. 2. **Clone the Repository**: Clone the repository to your local machine, e.g. using `git clone`. 3. **Install Node**: If you haven't already, install Node v20. -4. **Install pnpm**: If you haven't already, install pnpm v9. You can do this by running `npm install -g pnpm@9` if you're using npm. Alternatively, if you're using Homebrew (Mac), you can run `brew install pnpm`. For more see [the pnpm site](https://pnpm.io/installation). +4. **Install pnpm**: If you haven't already, install pnpm v10. You can do this by running `npm install -g pnpm@10` if you're using npm. Alternatively, if you're using Homebrew (Mac), you can run `brew install pnpm`. For more see [the pnpm site](https://pnpm.io/installation). 5. **Install Dependencies**: Navigate to the project directory and run `pnpm install` to install all necessary dependencies. 6. **Build the Project**: Run `pnpm build` in the root to build all packages. @@ -70,10 +70,21 @@ Some packages like `ai` also have more details tests and watch mode, see their ` We greatly appreciate your pull requests. Here are the steps to submit them: 1. **Create a New Branch**: Initiate your changes in a fresh branch. It's recommended to name the branch in a manner that signifies the changes you're implementing. -2. **Commit Your Changes**: Ensure your commits are succinct and clear, detailing what modifications have been made and the reasons behind them. -3. **Push the Changes to Your GitHub Repository**: After committing your changes, push them to your GitHub repository. -4. **Open a Pull Request**: Propose your changes for review. Furnish a lucid title and description of your contributions. Make sure to link any relevant issues your PR resolves. -5. **Respond to Feedback**: Stay receptive to and address any feedback or alteration requests from the project maintainers. +2. **Add a patch changeset**: If you're updating any packages and want to ensure they're released, add a **patch** changeset to your branch by running `pnpm changeset` in the workspace root. + + - **Please do not use minor or major changesets**, we'll let you know when you need to use a different changeset type than patch. + - You don't need to select any of the `examples/*` packages, as they are not released. + +3. **Commit Your Changes**: Ensure your commits are succinct and clear, detailing what modifications have been made and the reasons behind them. We don't require a specific commit message format, but please be descriptive. +4. **Fix prettier issues**: Run `pnpm prettier-fix` to fix any formatting issues in your code. +5. **Push the Changes to Your GitHub Repository**: After committing your changes, push them to your GitHub repository. +6. **Open a Pull Request**: Propose your changes for review. Furnish a lucid title and description of your contributions. Make sure to link any relevant issues your PR resolves. We use the following PR title format: + + - `fix(package-name): description` or + - `feat(package-name): description` or + - `chore(package-name): description` etc. + +7. **Respond to Feedback**: Stay receptive to and address any feedback or alteration requests from the project maintainers. ### Fixing Prettier Issues diff --git a/README.md b/README.md index f90be20a312e..ad93ed3ce3ec 120000 --- a/README.md +++ b/README.md @@ -1 +1 @@ -packages/ai/README.md \ No newline at end of file +packages/ai/README.md diff --git a/content/cookbook/01-next/23-chat-with-pdf.mdx b/content/cookbook/01-next/23-chat-with-pdf.mdx index 15deb7a3bf9f..71a212688cc4 100644 --- a/content/cookbook/01-next/23-chat-with-pdf.mdx +++ b/content/cookbook/01-next/23-chat-with-pdf.mdx @@ -10,8 +10,7 @@ Some language models like Anthropic's Claude Sonnet 3.5 and Google's Gemini 2.0 This example requires a provider that supports PDFs, such as Anthropic's - Claude Sonnet 3.5 or Google's Gemini 2.0. Note OpenAI's GPT-4o does not - currently support PDFs. Check the [provider + Claude 3.7, Google's Gemini 2.5, or OpenAI's GPT-4.1. Check the [provider documentation](/providers/ai-sdk-providers) for up-to-date support information. diff --git a/content/cookbook/01-next/24-stream-text-multistep.mdx b/content/cookbook/01-next/24-stream-text-multistep.mdx index fec93204aa78..bee5fac89d5f 100644 --- a/content/cookbook/01-next/24-stream-text-multistep.mdx +++ b/content/cookbook/01-next/24-stream-text-multistep.mdx @@ -54,7 +54,10 @@ export async function POST(req: Request) { system: 'You are a helpful assistant with a different system prompt. Repeat the extract user goal in your answer.', // continue the workflow stream with the messages from the previous step: - messages: [...messages, ...(await result1.response).messages], + messages: [ + ...convertToCoreMessages(messages), + ...(await result1.response).messages, + ], }); // forward the 2nd result to the client (incl. the finish event): diff --git a/content/cookbook/01-next/31-generate-object-with-file-prompt.mdx b/content/cookbook/01-next/31-generate-object-with-file-prompt.mdx index 34c3eba7a223..be9f3aaed399 100644 --- a/content/cookbook/01-next/31-generate-object-with-file-prompt.mdx +++ b/content/cookbook/01-next/31-generate-object-with-file-prompt.mdx @@ -94,7 +94,7 @@ export async function POST(request: Request) { }, ], schema: z.object({ - summary: z.string().describe('A 50 word sumamry of the PDF.'), + summary: z.string().describe('A 50 word summary of the PDF.'), }), }); diff --git a/content/cookbook/01-next/73-mcp-tools.mdx b/content/cookbook/01-next/73-mcp-tools.mdx index 36a58173a173..13570ea2fe7b 100644 --- a/content/cookbook/01-next/73-mcp-tools.mdx +++ b/content/cookbook/01-next/73-mcp-tools.mdx @@ -12,10 +12,15 @@ The AI SDK supports Model Context Protocol (MCP) tools by offering a lightweight Let's create a route handler for `/api/completion` that will generate text based on the input prompt and MCP tools that can be called at any time during a generation. The route will call the `streamText` function from the `ai` module, which will then generate text based on the input prompt and stream it to the client. +To use the `StreamableHTTPClientTransport`, you will need to install the official Typescript SDK for Model Context Protocol: + + + ```ts filename="app/api/completion/route.ts" import { experimental_createMCPClient, streamText } from 'ai'; import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio'; import { openai } from '@ai-sdk/openai'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'; export async function POST(req: Request) { const { prompt }: { prompt: string } = await req.json(); @@ -38,17 +43,17 @@ export async function POST(req: Request) { }, }); - // Similarly to the stdio example, you can pass in your own custom transport as long as it implements the `MCPTransport` interface: - const transport = new MyCustomTransport({ - // ... - }); - const customTransportClient = await experimental_createMCPClient({ + // Similarly to the stdio example, you can pass in your own custom transport as long as it implements the `MCPTransport` interface (e.g. `StreamableHTTPClientTransport`): + const transport = new StreamableHTTPClientTransport( + new URL('http://localhost:3000/mcp'), + ); + const customClient = await experimental_createMCPClient({ transport, }); const toolSetOne = await stdioClient.tools(); const toolSetTwo = await sseClient.tools(); - const toolSetThree = await customTransportClient.tools(); + const toolSetThree = await customClient.tools(); const tools = { ...toolSetOne, ...toolSetTwo, @@ -63,7 +68,15 @@ export async function POST(req: Request) { onFinish: async () => { await stdioClient.close(); await sseClient.close(); - await customTransportClient.close(); + await customClient.close(); + }, + // Closing clients onError is optional + // - Closing: Immediately frees resources, prevents hanging connections + // - Not closing: Keeps connection open for retries + onError: async error => { + await stdioClient.close(); + await sseClient.close(); + await customClient.close(); }, }); diff --git a/content/cookbook/05-node/51-call-tools-in-parallel.mdx b/content/cookbook/05-node/51-call-tools-in-parallel.mdx index 960884645d7c..e1b9cb8585ac 100644 --- a/content/cookbook/05-node/51-call-tools-in-parallel.mdx +++ b/content/cookbook/05-node/51-call-tools-in-parallel.mdx @@ -1,5 +1,5 @@ --- -title: Call Tools in Parallels +title: Call Tools in Parallel description: Learn how to call tools in parallel using the AI SDK and Node tags: ['node', 'tool use'] --- diff --git a/content/cookbook/05-node/80-local-caching-middleware.mdx b/content/cookbook/05-node/80-local-caching-middleware.mdx new file mode 100644 index 000000000000..e2a95a51287d --- /dev/null +++ b/content/cookbook/05-node/80-local-caching-middleware.mdx @@ -0,0 +1,242 @@ +--- +title: Local Caching Middleware +description: Learn how to create a caching middleware for local development. +tags: ['streaming', 'caching', 'middleware'] +--- + +# Local Caching Middleware + +When developing AI applications, you'll often find yourself repeatedly making the same API calls during development. This can lead to increased costs and slower development cycles. A caching middleware allows you to store responses locally and reuse them when the same inputs are provided. + +This approach is particularly useful in two scenarios: + +1. **Iterating on UI/UX** - When you're focused on styling and user experience, you don't want to regenerate AI responses for every code change. +2. **Working on evals** - When developing evals, you need to repeatedly test the same prompts, but don't need new generations each time. + +## Implementation + +In this implementation, you create a JSON file to store responses. When a request is made, you first check if you have already seen this exact request. If you have, you return the cached response immediately (as a one-off generation or chunks of tokens). If not, you trigger the generation, save the response, and return it. + + + Make sure to add the path of your local cache to your `.gitignore` so you do + not commit it. + + +### How it works + +For regular generations, you store and retrieve complete responses. Instead, the streaming implementation captures each token as it arrives, stores the full sequence, and on cache hits uses the SDK's `simulateReadableStream` utility to recreate the token-by-token streaming experience at a controlled speed (defaults to 10ms between chunks). + +This approach gives you the best of both worlds: + +- Instant responses for repeated queries +- Preserved streaming behavior for UI development + +The middleware handles all transformations needed to make cached responses indistinguishable from fresh ones, including normalizing tool calls and fixing timestamp formats. + +### Middleware + +```ts +import { + type LanguageModelV1, + type LanguageModelV1Middleware, + LanguageModelV1Prompt, + type LanguageModelV1StreamPart, + simulateReadableStream, + wrapLanguageModel, +} from 'ai'; +import 'dotenv/config'; +import fs from 'fs'; +import path from 'path'; + +const CACHE_FILE = path.join(process.cwd(), '.cache/ai-cache.json'); + +export const cached = (model: LanguageModelV1) => + wrapLanguageModel({ + middleware: cacheMiddleware, + model, + }); + +const ensureCacheFile = () => { + const cacheDir = path.dirname(CACHE_FILE); + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); + } + if (!fs.existsSync(CACHE_FILE)) { + fs.writeFileSync(CACHE_FILE, '{}'); + } +}; + +const getCachedResult = (key: string | object) => { + ensureCacheFile(); + const cacheKey = typeof key === 'object' ? JSON.stringify(key) : key; + try { + const cacheContent = fs.readFileSync(CACHE_FILE, 'utf-8'); + + const cache = JSON.parse(cacheContent); + + const result = cache[cacheKey]; + + return result ?? null; + } catch (error) { + console.error('Cache error:', error); + return null; + } +}; + +const updateCache = (key: string, value: any) => { + ensureCacheFile(); + try { + const cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8')); + const updatedCache = { ...cache, [key]: value }; + fs.writeFileSync(CACHE_FILE, JSON.stringify(updatedCache, null, 2)); + console.log('Cache updated for key:', key); + } catch (error) { + console.error('Failed to update cache:', error); + } +}; +const cleanPrompt = (prompt: LanguageModelV1Prompt) => { + return prompt.map(m => { + if (m.role === 'assistant') { + return m.content.map(part => + part.type === 'tool-call' ? { ...part, toolCallId: 'cached' } : part, + ); + } + if (m.role === 'tool') { + return m.content.map(tc => ({ + ...tc, + toolCallId: 'cached', + result: {}, + })); + } + + return m; + }); +}; + +export const cacheMiddleware: LanguageModelV1Middleware = { + wrapGenerate: async ({ doGenerate, params }) => { + const cacheKey = JSON.stringify({ + ...cleanPrompt(params.prompt), + _function: 'generate', + }); + console.log('Cache Key:', cacheKey); + + const cached = getCachedResult(cacheKey) as Awaited< + ReturnType + > | null; + + if (cached && cached !== null) { + console.log('Cache Hit'); + return { + ...cached, + response: { + ...cached.response, + timestamp: cached?.response?.timestamp + ? new Date(cached?.response?.timestamp) + : undefined, + }, + }; + } + + console.log('Cache Miss'); + const result = await doGenerate(); + + updateCache(cacheKey, result); + + return result; + }, + wrapStream: async ({ doStream, params }) => { + const cacheKey = JSON.stringify({ + ...cleanPrompt(params.prompt), + _function: 'stream', + }); + console.log('Cache Key:', cacheKey); + + // Check if the result is in the cache + const cached = getCachedResult(cacheKey); + + // If cached, return a simulated ReadableStream that yields the cached result + if (cached && cached !== null) { + console.log('Cache Hit'); + // Format the timestamps in the cached response + const formattedChunks = (cached as LanguageModelV1StreamPart[]).map(p => { + if (p.type === 'response-metadata' && p.timestamp) { + return { ...p, timestamp: new Date(p.timestamp) }; + } else return p; + }); + return { + stream: simulateReadableStream({ + initialDelayInMs: 0, + chunkDelayInMs: 10, + chunks: formattedChunks, + }), + rawCall: { rawPrompt: null, rawSettings: {} }, + }; + } + + console.log('Cache Miss'); + // If not cached, proceed with streaming + const { stream, ...rest } = await doStream(); + + const fullResponse: LanguageModelV1StreamPart[] = []; + + const transformStream = new TransformStream< + LanguageModelV1StreamPart, + LanguageModelV1StreamPart + >({ + transform(chunk, controller) { + fullResponse.push(chunk); + controller.enqueue(chunk); + }, + flush() { + // Store the full response in the cache after streaming is complete + updateCache(cacheKey, fullResponse); + }, + }); + + return { + stream: stream.pipeThrough(transformStream), + ...rest, + }; + }, +}; +``` + +## Using the Middleware + +The middleware can be easily integrated into your existing AI SDK setup: + +```ts highlight="4,8" +import { openai } from '@ai-sdk/openai'; +import { streamText } from 'ai'; +import 'dotenv/config'; +import { cached } from '../middleware/your-cache-middleware'; + +async function main() { + const result = streamText({ + model: cached(openai('gpt-4o')), + maxTokens: 512, + temperature: 0.3, + maxRetries: 5, + prompt: 'Invent a new holiday and describe its traditions.', + }); + + for await (const textPart of result.textStream) { + process.stdout.write(textPart); + } + + console.log(); + console.log('Token usage:', await result.usage); + console.log('Finish reason:', await result.finishReason); +} + +main().catch(console.error); +``` + +## Considerations + +When using this caching middleware, keep these points in mind: + +1. **Development Only** - This approach is intended for local development, not production environments +2. **Cache Invalidation** - You'll need to clear the cache (delete the cache file) when you want fresh responses +3. **Multi-Step Flows** - When using `maxSteps`, be aware that caching occurs at the individual language model response level, not across the entire execution flow. This means that while the model's generation is cached, the tool call is not and will run on each generation. diff --git a/content/docs/01-introduction/index.mdx b/content/docs/00-introduction/index.mdx similarity index 72% rename from content/docs/01-introduction/index.mdx rename to content/docs/00-introduction/index.mdx index b53909739d58..daa2ae80c2f8 100644 --- a/content/docs/01-introduction/index.mdx +++ b/content/docs/00-introduction/index.mdx @@ -11,6 +11,14 @@ The AI SDK is the TypeScript toolkit designed to help developers build AI-powere Integrating large language models (LLMs) into applications is complicated and heavily dependent on the specific model provider you use. +The AI SDK standardizes integrating artificial intelligence (AI) models across [supported providers](/docs/foundations/providers-and-models). This enables developers to focus on building great AI applications, not waste time on technical details. + +For example, here’s how you can generate text with various models using the AI SDK: + + + +The AI SDK has two main libraries: + - **[AI SDK Core](/docs/ai-sdk-core):** A unified API for generating text, structured objects, tool calls, and building agents with LLMs. - **[AI SDK UI](/docs/ai-sdk-ui):** A set of framework-agnostic hooks for quickly building chat and generative user interface. @@ -48,15 +56,15 @@ We've built some [templates](https://vercel.com/templates?type=ai) that include If you have questions about anything related to the AI SDK, you're always welcome to ask our community on [GitHub Discussions](https://github.com/vercel/ai/discussions). -## `llms.txt` +## `llms.txt` (for Cursor, Windsurf, Copilot, Claude etc.) -You can access the entire AI SDK documentation in Markdown format at [sdk.vercel.ai/llms.txt](/llms.txt). This can be used to ask any LLM (assuming it has a big enough context window) questions about the AI SDK based on the most up-to-date documentation. +You can access the entire AI SDK documentation in Markdown format at [ai-sdk.dev/llms.txt](/llms.txt). This can be used to ask any LLM (assuming it has a big enough context window) questions about the AI SDK based on the most up-to-date documentation. ### Example Usage For instance, to prompt an LLM with questions about the AI SDK: -1. Copy the documentation contents from [sdk.vercel.ai/llms.txt](/llms.txt) +1. Copy the documentation contents from [ai-sdk.dev/llms.txt](/llms.txt) 2. Use the following prompt format: ```prompt diff --git a/content/docs/01-announcing-ai-sdk-5-alpha/index.mdx b/content/docs/01-announcing-ai-sdk-5-alpha/index.mdx new file mode 100644 index 000000000000..eac664b1e837 --- /dev/null +++ b/content/docs/01-announcing-ai-sdk-5-alpha/index.mdx @@ -0,0 +1,379 @@ +--- +title: AI SDK 5 Alpha +description: Get started with the Alpha version of AI SDK 5. +--- + +# Announcing AI SDK 5 Alpha + + + This is an early preview — AI SDK 5 is under active development. APIs may + change without notice. Pin to specific versions as breaking changes may occur + even in patch releases. + + +## Alpha Version Guidance + +The AI SDK 5 Alpha is intended for: + +- Exploration and early prototypes +- Green-field projects where you can experiment freely +- Development environments where you can tolerate breaking changes + +This Alpha release is **not recommended** for: + +- Production applications +- Projects that require stable APIs +- Existing applications that would need migration paths + +During this Alpha phase, we expect to make significant, potentially breaking changes to the API surface. We're sharing early to gather feedback and improve the SDK before stabilization. Your input is invaluable—please share your experiences through GitHub issues or discussions to help shape the final release. + + + We expect bugs in this Alpha release. To help us improve the SDK, please [file + bug reports on GitHub](https://github.com/vercel/ai/issues/new/choose). Your + reports directly contribute to making the final release more stable and + reliable. + + +## Installation + +To install the AI SDK 5 - Alpha, run the following command: + +```bash +# replace with your provider and framework +npm install ai@alpha @ai-sdk/[your-provider]@alpha @ai-sdk/[your-framework]@alpha +``` + + + APIs may change without notice. Pin to specific versions as breaking changes + may occur even in patch releases. + + +## What's new in AI SDK 5? + +AI SDK 5 is a complete redesign of the AI SDK's protocol and architecture based on everything we've learned over the last two years of real-world usage. We've also modernized the UI and protocols that have remained largely unchanged since AI SDK v2/3, creating a strong foundation for the future. + +### Why AI SDK 5? + +When we originally designed the v1 protocol over a year ago, the standard interaction pattern with language models was simple: text in, text or tool call out. But today's LLMs go way beyond text and tool calls, generating reasoning, sources, images and more. Additionally, new use-cases like computer using agents introduce a fundamentally new approach to interacting with language models that made it near-impossible to support in a unified approach with our original architecture. + +We needed a protocol designed for this new reality. While this is a breaking change that we don't take lightly, it's provided an opportunity to rebuild the foundation and add powerful new features. + +While we've designed AI SDK 5 to be a substantial improvement over previous versions, we're still in active development. You might encounter bugs or unexpected behavior. We'd greatly appreciate your feedback and bug reports—they're essential to making this release better. Please share your experiences and suggestions with us through [GitHub issues](https://github.com/vercel/ai/issues/new/choose) or [GitHub discussions](https://github.com/vercel/ai/discussions). + +## New Features + +- [**LanguageModelV2**](#languagemodelv2) - new redesigned architecture +- [**Message Overhaul**](#message-overhaul) - new `UIMessage` and `ModelMessage` types +- [**ChatStore**](#chatstore) - new `useChat` architecture +- [**Server-Sent Events (SSE)**](#server-sent-events-sse) - new standardised protocol for sending UI messages to the client +- [**Agentic Control**](#agentic-control) - new primitives for building agentic systems + +## LanguageModelV2 + +LanguageModelV2 represents a complete redesign of how the AI SDK communicates with language models, adapting to the increasingly complex outputs modern AI systems generate. The new LanguageModelV2 treats all LLM outputs as content parts, enabling more consistent handling of text, images, reasoning, sources, and other response types. It now has: + +- **Content-First Design** - Rather than separating text, reasoning, and tool calls, everything is now represented as ordered content parts in a unified array +- **Improved Type Safety** - The new LanguageModelV2 provides better TypeScript type guarantees, making it easier to work with different content types +- **Simplified Extensibility** - Adding support for new model capabilities no longer requires changes to the core structure + +## Message Overhaul + +AI SDK 5 introduces a completely redesigned message system with two message types that address the dual needs of what you render in your UI and what you send to the model. Context is crucial for effective language model generations, and these two message types serve distinct purposes: + +- **UIMessage** represents the complete conversation history for your interface, preserving all message parts (text, images, data), metadata (creation timestamps, generation times), and UI state—regardless of length. + +- **ModelMessage** is optimized for sending to language models, considering token input constraints. It strips away UI-specific metadata and irrelevant content. + +With this change, you will be required to explicitly convert your `UIMessage`s to `ModelMessage`s before sending them to the model. + +```ts highlight="9" +import { openai } from '@ai-sdk/openai'; +import { convertToModelMessages, streamText, UIMessage } from 'ai'; + +export async function POST(req: Request) { + const { messages }: { messages: UIMessage[] } = await req.json(); + + const result = streamText({ + model: openai('gpt-4o'), + messages: convertToModelMessages(messages), + }); + + return result.toUIMessageStreamResponse(); +} +``` + + + This separation is essential as you cannot use a single message format for + both purposes. The state you save should always be the UIMessage format to + prevent information loss, with explicit conversion to ModelMessage when + communicating with language models. + + +The new message system has made possible several highly requested features: + +- **Type-safe Message Metadata** - add structured information per message +- **New Stream Writer** - stream any part type (reasoning, sources, etc.) retaining proper order +- **Data Parts** - stream type-safe arbitrary data parts for dynamic UI components + +### Message metadata + +Metadata allows you to attach structured information to individual messages, making it easier to track important details like response time, token usage, or model specifications. This information can enhance your UI with contextual data without embedding it in the message content itself. + +To add metadata to a message, first define the metadata schema: + +```ts filename="app/api/chat/example-metadata-schema.ts" +export const exampleMetadataSchema = z.object({ + duration: z.number().optional(), + model: z.string().optional(), + totalTokens: z.number().optional(), +}); + +export type ExampleMetadata = z.infer; +``` + +Then add the metadata using the `message.metadata` property on the `toUIMessageStreamResponse()` utility: + +```ts filename="app/api/chat/route.ts" +import { openai } from '@ai-sdk/openai'; +import { convertToModelMessages, streamText, UIMessage } from 'ai'; +import { ExampleMetadata } from './example-metadata-schema'; + +export async function POST(req: Request) { + const { messages }: { messages: UIMessage[] } = await req.json(); + + const startTime = Date.now(); + const result = streamText({ + model: openai('gpt-4o'), + prompt: convertToModelMessages(messages), + }); + + return result.toUIMessageStreamResponse({ + messageMetadata: ({ part }): ExampleMetadata | undefined => { + // send custom information to the client on start: + if (part.type === 'start') { + return { + model: 'gpt-4o', // initial model id + }; + } + + // send additional model information on finish-step: + if (part.type === 'finish-step') { + return { + model: part.response.modelId, // update with the actual model id + duration: Date.now() - startTime, + }; + } + + // when the message is finished, send additional information: + if (part.type === 'finish') { + return { + totalTokens: part.totalUsage.totalTokens, + }; + } + }, + }); +} +``` + +Finally, specify the message metadata schema on the client and then render the (type-safe) metadata in your UI: + +```tsx filename="app/page.tsx" +import { zodSchema } from '@ai-sdk/provider-utils'; +import { useChat } from '@ai-sdk/react'; +import { defaultChatStore } from 'ai'; +import { exampleMetadataSchema } from '@/api/chat/example-metadata-schema'; + +export default function Chat() { + const { messages } = useChat({ + chatStore: defaultChatStore({ + api: '/api/use-chat', + messageMetadataSchema: zodSchema(exampleMetadataSchema), + }), + }); + + return ( +
+ {messages.map(message => { + const { metadata } = message; + return ( +
+ {metadata?.duration &&
Duration: {metadata.duration}ms
} + {metadata?.model &&
Model: {metadata.model}
} + {metadata?.totalTokens && ( +
Total tokens: {metadata.totalTokens}
+ )} +
+ ); + })} +
+ ); +} +``` + +### UI Message Stream + +The UI Message Stream enables streaming any content parts from the server to the client. With this stream, you can send structured data like custom sources from your RAG pipeline directly to your UI. The stream writer is simply a utility that makes it easy to write to this message stream. + +```ts +const stream = createUIMessageStream({ + execute: writer => { + // stream custom sources + writer.write({ + type: 'source', + value: { + type: 'source', + sourceType: 'url', + id: 'source-1', + url: 'https://example.com', + title: 'Example Source', + }, + }); + }, +}); +``` + +On the client, these will be added to the ordered `message.parts` array. + +### Data Parts + +The new stream writer also enables a type-safe way to stream arbitrary data from the server to the client and display it in your UI. + +You can create and stream custom data parts on the server: + +```tsx +// On the server +const stream = createUIMessageStream({ + execute: writer => { + // Initial update + writer.write({ + type: 'data-weather', // Custom type + id: toolCallId, // ID for updates + data: { city, status: 'loading' }, // Your data + }); + + // Later, update the same part + writer.write({ + type: 'data-weather', + id: toolCallId, + data: { city, weather, status: 'success' }, + }); + }, +}); +``` + +On the client, you can render these parts with full type safety: + +```tsx +{ + message.parts + .filter(part => part.type === 'data-weather') // type-safe + .map((part, index) => ( + + )); +} +``` + +Data parts appear in the `message.parts` array along with other content, maintaining the proper ordering of the conversation. You can update parts by referencing the same ID, enabling dynamic experiences like collaborative artifacts. + +## ChatStore + +AI SDK 5 introduces a new `useChat` architecture with ChatStore and ChatTransport components. These two core building blocks make state management and API integration more flexible, allowing you to compose reactive UI bindings, share chat state across multiple instances, and swap out your backend protocol without rewriting application logic. + +The `ChatStore` is responsible for: + +- **Managing multiple chats** – access and switch between conversations seamlessly. +- **Processing response streams** – handle streams from the server and synchronize state (e.g. when there are concurrent client-side tool results). +- **Caching and synchronizing** – share state (messages, status, errors) between `useChat` hooks. + +You can create a basic ChatStore with the helper function: + +```ts +import { defaultChatStore } from 'ai'; + +const chatStore = defaultChatStore({ + api: '/api/chat', // your chat endpoint + maxSteps: 5, // optional: limit LLM calls in tool chains + chats: {}, // optional: preload previous chat sessions +}); + +import { useChat } from '@ai-sdk/react'; +const { messages, input, handleSubmit } = useChat({ chatStore }); +``` + +## Server-Sent Events (SSE) + +AI SDK 5 now uses Server-Sent Events (SSE) instead of a custom streaming protocol. SSE is a common web standard for sending data from servers to browsers. This switch has several advantages: + +- **Works everywhere** - Uses technology that works in all major browsers and platforms +- **Easier to troubleshoot** - You can see the data stream in browser developer tools +- **Simple to build upon** - Adding new features is more straightforward +- **More stable** - Built on proven technology that many developers already use + +## Agentic Control + +AI SDK 5 introduces new features for building agents that help you control model behavior more precisely. + +### prepareStep + +The `prepareStep` function gives you fine-grained control over each step in a multi-step agent. It's called before a step starts and allows you to: + +- Dynamically change the model used for specific steps +- Force specific tool selections for particular steps +- Limit which tools are available during specific steps +- Examine the context of previous steps before proceeding + +```ts +const result = await generateText({ + // ... + experimental_prepareStep: async ({ model, stepNumber, maxSteps, steps }) => { + if (stepNumber === 0) { + return { + // use a different model for this step: + model: modelForThisParticularStep, + // force a tool choice for this step: + toolChoice: { type: 'tool', toolName: 'tool1' }, + // limit the tools that are available for this step: + experimental_activeTools: ['tool1'], + }; + } + // when nothing is returned, the default settings are used + }, +}); +``` + +This makes it easier to build AI systems that adapt their capabilities based on the current context and task requirements. + +### continueUntil + +The `continueUntil` parameter lets you define stopping conditions for your agent. Instead of running indefinitely, you can specify exactly when the agent should terminate based on various conditions: + +- Reaching a maximum number of steps +- Calling a specific tool +- Satisfying any custom condition you define + +```ts +const result = generateText({ + // ... + // stop loop at 5 steps + continueUntil: maxSteps(5), +}); + +const result = generateText({ + // ... + // stop loop when weather tool called + continueUntil: hasToolCall('weather'), +}); + +const result = generateText({ + // ... + // stop loop at your own custom condition + continueUntil: maxTotalTokens(20000), +}); +``` + +These agentic controls form the foundation for building more reliable, controllable AI systems that can tackle complex problems while remaining within well-defined constraints. diff --git a/content/docs/02-foundations/02-providers-and-models.mdx b/content/docs/02-foundations/02-providers-and-models.mdx index a1902d339608..a87bee6fe6d0 100644 --- a/content/docs/02-foundations/02-providers-and-models.mdx +++ b/content/docs/02-foundations/02-providers-and-models.mdx @@ -40,6 +40,13 @@ The AI SDK comes with a wide range of providers that you can use to interact wit - [Cerebras Provider](/providers/ai-sdk-providers/cerebras) (`@ai-sdk/cerebras`) - [Groq Provider](/providers/ai-sdk-providers/groq) (`@ai-sdk/groq`) - [Perplexity Provider](/providers/ai-sdk-providers/perplexity) (`@ai-sdk/perplexity`) +- [ElevenLabs Provider](/providers/ai-sdk-providers/elevenlabs) (`@ai-sdk/elevenlabs`) +- [LMNT Provider](/providers/ai-sdk-providers/lmnt) (`@ai-sdk/lmnt`) +- [Hume Provider](/providers/ai-sdk-providers/hume) (`@ai-sdk/hume`) +- [Rev.ai Provider](/providers/ai-sdk-providers/revai) (`@ai-sdk/revai`) +- [Deepgram Provider](/providers/ai-sdk-providers/deepgram) (`@ai-sdk/deepgram`) +- [Gladia Provider](/providers/ai-sdk-providers/gladia) (`@ai-sdk/gladia`) +- [AssemblyAI Provider](/providers/ai-sdk-providers/assemblyai) (`@ai-sdk/assemblyai`) You can also use the [OpenAI Compatible provider](/providers/openai-compatible-providers) with OpenAI-compatible APIs: @@ -60,9 +67,12 @@ The open-source community has created the following providers: - [Mixedbread Provider](/providers/community-providers/mixedbread) (`mixedbread-ai-provider`) - [Voyage AI Provider](/providers/community-providers/voyage-ai) (`voyage-ai-provider`) - [Mem0 Provider](/providers/community-providers/mem0)(`@mem0/vercel-ai-provider`) +- [Letta Provider](/providers/community-providers/letta)(`@letta-ai/vercel-ai-sdk-provider`) - [Spark Provider](/providers/community-providers/spark) (`spark-ai-provider`) - [AnthropicVertex Provider](/providers/community-providers/anthropic-vertex-ai) (`anthropic-vertex-ai`) - [LangDB Provider](/providers/community-providers/langdb) (`@langdb/vercel-provider`) +- [Dify Provider](/providers/community-providers/dify) (`dify-ai-provider`) +- [Sarvam Provider](/providers/community-providers/sarvam) (`sarvam-ai-provider`) ## Self-Hosted Models @@ -79,43 +89,56 @@ Additionally, any self-hosted provider that supports the OpenAI specification ca The AI providers support different language models with various capabilities. Here are the capabilities of popular models: -| Provider | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | -| ------------------------------------------------------------------------ | ---------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | -| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-1212` | | | | | -| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-vision-1212` | | | | | -| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-beta` | | | | | -| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-vision-beta` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o-mini` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4-turbo` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `o3-mini` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `o1` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `o1-mini` | | | | | -| [OpenAI](/providers/ai-sdk-providers/openai) | `o1-preview` | | | | | -| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-7-sonnet-20250219` | | | | | -| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20241022` | | | | | -| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20240620` | | | | | -| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-haiku-20241022` | | | | | -| [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-large-latest` | | | | | -| [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-large-latest` | | | | | -| [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-small-latest` | | | | | -| [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-12b-2409` | | | | | -| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-2.0-flash-exp` | | | | | -| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-flash` | | | | | -| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-pro` | | | | | -| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-2.0-flash-exp` | | | | | -| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-flash` | | | | | -| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-pro` | | | | | -| [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-chat` | | | | | -| [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-reasoner` | | | | | -| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-8b` | | | | | -| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-70b` | | | | | -| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.3-70b` | | | | | -| [Groq](/providers/ai-sdk-providers/groq) | `llama-3.3-70b-versatile` | | | | | -| [Groq](/providers/ai-sdk-providers/groq) | `llama-3.1-8b-instant` | | | | | -| [Groq](/providers/ai-sdk-providers/groq) | `mixtral-8x7b-32768` | | | | | -| [Groq](/providers/ai-sdk-providers/groq) | `gemma2-9b-it` | | | | | +| Provider | Model | Image Input | Object Generation | Tool Usage | Tool Streaming | +| ------------------------------------------------------------------------ | ------------------------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-fast` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-3-mini-fast` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-1212` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-2-vision-1212` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-beta` | | | | | +| [xAI Grok](/providers/ai-sdk-providers/xai) | `grok-vision-beta` | | | | | +| [Vercel](/providers/ai-sdk-providers/vercel) | `v0-1.0-md` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-mini` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4.1-nano` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4o-mini` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4-turbo` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `gpt-4` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o3-mini` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o3` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o4-mini` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o1` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o1-mini` | | | | | +| [OpenAI](/providers/ai-sdk-providers/openai) | `o1-preview` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-4-opus-20250514` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-4-sonnet-20250514` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-7-sonnet-20250219` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20241022` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-sonnet-20240620` | | | | | +| [Anthropic](/providers/ai-sdk-providers/anthropic) | `claude-3-5-haiku-20241022` | | | | | +| [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-large-latest` | | | | | +| [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-large-latest` | | | | | +| [Mistral](/providers/ai-sdk-providers/mistral) | `mistral-small-latest` | | | | | +| [Mistral](/providers/ai-sdk-providers/mistral) | `pixtral-12b-2409` | | | | | +| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-2.0-flash-exp` | | | | | +| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-flash` | | | | | +| [Google Generative AI](/providers/ai-sdk-providers/google-generative-ai) | `gemini-1.5-pro` | | | | | +| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-2.0-flash-exp` | | | | | +| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-flash` | | | | | +| [Google Vertex](/providers/ai-sdk-providers/google-vertex) | `gemini-1.5-pro` | | | | | +| [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-chat` | | | | | +| [DeepSeek](/providers/ai-sdk-providers/deepseek) | `deepseek-reasoner` | | | | | +| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-8b` | | | | | +| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.1-70b` | | | | | +| [Cerebras](/providers/ai-sdk-providers/cerebras) | `llama3.3-70b` | | | | | +| [Groq](/providers/ai-sdk-providers/groq) | `meta-llama/llama-4-scout-17b-16e-instruct` | | | | | +| [Groq](/providers/ai-sdk-providers/groq) | `llama-3.3-70b-versatile` | | | | | +| [Groq](/providers/ai-sdk-providers/groq) | `llama-3.1-8b-instant` | | | | | +| [Groq](/providers/ai-sdk-providers/groq) | `mixtral-8x7b-32768` | | | | | +| [Groq](/providers/ai-sdk-providers/groq) | `gemma2-9b-it` | | | | | This table is not exhaustive. Additional models can be found in the provider diff --git a/content/docs/02-foundations/03-prompts.mdx b/content/docs/02-foundations/03-prompts.mdx index a7ad75f94d8a..e1759e3976d2 100644 --- a/content/docs/02-foundations/03-prompts.mdx +++ b/content/docs/02-foundations/03-prompts.mdx @@ -92,6 +92,84 @@ Instead of sending a text in the `content` property, you can send an array of pa models](./providers-and-models#model-capabilities). +### Provider Options + +You can pass through additional provider-specific metadata to enable provider-specific functionality at 3 levels. + +#### Function Call Level + +Functions like [`streamText`](/docs/reference/ai-sdk-core/stream-text#provider-options) or [`generateText`](/docs/reference/ai-sdk-core/generate-text#provider-options) accept a `providerOptions` property. + +Adding provider options at the function call level should be used when you do not need granular control over where the provider options are applied. + +```ts +const { text } = await generateText({ + model: azure('your-deployment-name'), + providerOptions: { + openai: { + reasoningEffort: 'low', + }, + }, +}); +``` + +#### Message Level + +For granular control over applying provider options at the message level, you can pass `providerOptions` to the message object: + +```ts +const messages = [ + { + role: 'system', + content: 'Cached system message', + providerOptions: { + // Sets a cache control breakpoint on the system message + anthropic: { cacheControl: { type: 'ephemeral' } }, + }, + }, +]; +``` + +#### Message Part Level + +Certain provider-specific options require configuration at the message part level: + +```ts +const messages = [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Describe the image in detail.', + providerOptions: { + openai: { imageDetail: 'low' }, + }, + }, + { + type: 'image', + image: + 'https://github.com/vercel/ai/blob/main/examples/ai-core/data/comic-cat.png?raw=true', + // Sets image detail configuration for image part: + providerOptions: { + openai: { imageDetail: 'low' }, + }, + }, + ], + }, +]; +``` + + + AI SDK UI hooks like [`useChat`](/docs/reference/ai-sdk-ui/use-chat) return + arrays of `UIMessage` objects, which do not support provider options. We + recommend using the + [`convertToCoreMessages`](/docs/reference/ai-sdk-ui/convert-to-core-messages) + function to convert `UIMessage` objects to + [`CoreMessage`](/docs/reference/ai-sdk-core/core-message) objects before + applying or appending message(s) or message parts with `providerOptions`. + + ### User Messages #### Text Parts diff --git a/content/docs/02-foundations/04-tools.mdx b/content/docs/02-foundations/04-tools.mdx index ac8a2acf3338..c6a21923a030 100644 --- a/content/docs/02-foundations/04-tools.mdx +++ b/content/docs/02-foundations/04-tools.mdx @@ -93,7 +93,9 @@ There are several providers that offer pre-built tools as **toolkits** that you - **[agentic](https://github.com/transitive-bullshit/agentic)** - A collection of 20+ tools. Most tools connect to access external APIs such as [Exa](https://exa.ai/) or [E2B](https://e2b.dev/). - **[browserbase](https://docs.browserbase.com/integrations/vercel-ai/introduction)** - Browser tool that runs a headless browser +- **[browserless](https://docs.browserless.io/ai-integrations/vercel-ai-sdk)** - Browser automation service with AI integration - self hosted or cloud based - **[Stripe agent tools](https://docs.stripe.com/agents)** - Tools for interacting with Stripe. +- **[StackOne ToolSet](https://docs.stackone.com/agents)** - Agentic integrations for hundreds of [enterprise SaaS](https://www.stackone.com/integrations) - **[Toolhouse](https://docs.toolhouse.ai/toolhouse/using-vercel-ai)** - AI function-calling in 3 lines of code for over 25 different actions. - **[Agent Tools](https://ai-sdk-agents.vercel.app/?item=introduction)** - A collection of tools for agents. - **[AI Tool Maker](https://github.com/nihaocami/ai-tool-maker)** - A CLI utility to generate AI SDK tools from OpenAPI specs. diff --git a/content/docs/02-getting-started/02-nextjs-app-router.mdx b/content/docs/02-getting-started/02-nextjs-app-router.mdx index 8a8bc840e606..95451e468dc2 100644 --- a/content/docs/02-getting-started/02-nextjs-app-router.mdx +++ b/content/docs/02-getting-started/02-nextjs-app-router.mdx @@ -234,17 +234,17 @@ In this updated code: - Defines parameters using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this parameter from the context of the conversation. If it can't, it will ask the user for the missing information. - Defines an `execute` function that simulates getting weather data (in this case, it returns a random temperature). This is an asynchronous function running on the server so you can fetch real data from an external API. - Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary parameters. The `execute` function will then be automatically run, and you can access the results via `toolInvocations` that is available on the message object. +Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary parameters. The `execute` function will then be automatically run, and you can access the results via `tool-invocations` part that is available on the `message.parts` array. Try asking something like "What's the weather in New York?" and see how the model uses the new tool. -Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result in the `toolInvocations` key of the message object. +Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result via the `tool-invocation` part of the `message.parts` array. ### Update the UI To display the tool invocations in your UI, update your `app/page.tsx` file: -```tsx filename="app/page.tsx" highlight="18-24" +```tsx filename="app/page.tsx" highlight="16-21" 'use client'; import { useChat } from '@ai-sdk/react'; diff --git a/content/docs/02-getting-started/04-svelte.mdx b/content/docs/02-getting-started/04-svelte.mdx index 36bd75871702..6de2e30f49c4 100644 --- a/content/docs/02-getting-started/04-svelte.mdx +++ b/content/docs/02-getting-started/04-svelte.mdx @@ -139,8 +139,17 @@ Update your root page (`src/routes/+page.svelte`) with the following code to sho
    - {#each chat.messages as message} -
  • {message.role}: {message.content}
  • + {#each chat.messages as message, messageIndex (messageIndex)} +
  • +
    {message.role}
    +
    + {#each message.parts as part, partIndex (partIndex)} + {#if part.type === 'text'} +
    {part.text}
    + {/if} + {/each} +
    +
  • {/each}
@@ -152,10 +161,12 @@ Update your root page (`src/routes/+page.svelte`) with the following code to sho This page utilizes the `Chat` class, which will, by default, use the `POST` route handler you created earlier. The hook provides functions and state for handling user input and form submission. The `Chat` class provides multiple utility functions and state variables: -- `messages` - the current chat messages (an array of objects with `id`, `role`, and `content` properties). +- `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties). - `input` - the current value of the user's input field. - `handleSubmit` - function to handle form submission. +The LLM's response is accessed through the message `parts` array. Each message contains an ordered array of `parts` that represents everything the model generated in its response. These parts can include plain text, reasoning tokens, and more that you will see later. The `parts` array preserves the sequence of the model's outputs, allowing you to display or process each component in the order it was generated. + ## Running Your Application With that, you have built everything you need for your chatbot! To start your application, use the command: @@ -225,11 +236,11 @@ In this updated code: - Defines parameters using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this parameter from the context of the conversation. If it can't, it will ask the user for the missing information. - Defines an `execute` function that simulates getting weather data (in this case, it returns a random temperature). This is an asynchronous function running on the server so you can fetch real data from an external API. -Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary parameters. The `execute` function will then be automatically run, and you can access the results via `toolInvocations` that is available on the message object. +Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary parameters. The `execute` function will then be automatically run, and you can access the results via `tool-invocations` part that is available on the `message.parts` array. Try asking something like "What's the weather in New York?" and see how the model uses the new tool. -Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result in the `toolInvocations` key of the message object. +Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result via the `tool-invocation` part of the `message.parts` array. ### Update the UI @@ -244,14 +255,18 @@ To display the tool invocations in your UI, update your `src/routes/+page.svelte
    - {#each chat.messages as message} + {#each chat.messages as message, messageIndex (messageIndex)}
  • - {message.role}: - {#if message.toolInvocations} -
    {JSON.stringify(message.toolInvocations, null, 2)}
    - {:else} - {message.content} - {/if} +
    {message.role}
    +
    + {#each message.parts as part, partIndex (partIndex)} + {#if part.type === 'text'} +
    {part.text}
    + {:else if part.type === 'tool-invocation'} +
    {JSON.stringify(part.toolInvocation, null, 2)}
    + {/if} + {/each} +
  • {/each}
@@ -262,7 +277,7 @@ To display the tool invocations in your UI, update your `src/routes/+page.svelte
``` -With this change, you check each message for any tool calls (`toolInvocations`). These tool calls will be displayed as stringified JSON. Otherwise, you show the message content as before. +With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For tool invocations, you display a JSON representation of the tool call and its result. Now, when you ask about the weather, you'll see the tool invocation and its result displayed in your chat interface. diff --git a/content/docs/02-getting-started/05-nuxt.mdx b/content/docs/02-getting-started/05-nuxt.mdx index 72ac0e1b7475..ed58a7f22d04 100644 --- a/content/docs/02-getting-started/05-nuxt.mdx +++ b/content/docs/02-getting-started/05-nuxt.mdx @@ -24,7 +24,7 @@ If you haven't obtained your OpenAI API key, you can do so by [signing up](https Start by creating a new Nuxt application. This command will create a new directory named `my-ai-app` and set up a basic Nuxt application inside it. - + Navigate to the newly created directory: @@ -137,15 +137,19 @@ const { messages, input, handleSubmit } = useChat();