diff --git a/.changeset/clean-jobs-smile.md b/.changeset/clean-jobs-smile.md new file mode 100644 index 000000000000..165cc6369570 --- /dev/null +++ b/.changeset/clean-jobs-smile.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/google': patch +--- + +Enhance Google provider: improve `cachedContent` handling by conditionally sending parameters, expose detailed token usage metadata (including cached, thoughts, prompt, cache, candidates, and tool use details by modality), and enable `mediaResolution` setting. 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..163960165d9b 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: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 42a185321759..6214494aba89 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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12eec2f442e2..6e86f79a4bc9 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' 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/CONTRIBUTING.md b/CONTRIBUTING.md index d9644053c4a2..6ed072f3e44e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/01-introduction/index.mdx index b53909739d58..daa2ae80c2f8 100644 --- a/content/docs/01-introduction/index.mdx +++ b/content/docs/01-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/02-foundations/02-providers-and-models.mdx b/content/docs/02-foundations/02-providers-and-models.mdx index a1902d339608..13e466cd27f5 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,53 @@ 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` | | | | | +| [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-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();